From 342b490fe740d0ec3d0b779c8c85a34e5e3c2132 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 10 Feb 2026 09:48:51 +0100 Subject: [PATCH] feat: Task decomposition for Tailwind/Radix/shadcn foundation setup Decomposed "Foundation Setup - Install Dependencies & Configure Tailwind" phase into 6 executable tasks: 1. Install Tailwind CSS, PostCSS & Autoprefixer 2. Map MUI theme to Tailwind design tokens 3. Setup CSS variables for dynamic theming 4. Install Radix UI primitives 5. Initialize shadcn/ui and setup component directory 6. Move MUI to devDependencies and verify setup Tasks follow logical dependency chain with final human verification checkpoint before proceeding with component migration. Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 60 ++-- docs/agent.md | 103 +++++++ docs/architecture.md | 109 ++++++++ .../{ => archive}/agent-lifecycle-refactor.md | 0 docs/{ => archive}/agents/architect.md | 0 docs/{ => archive}/agents/verifier.md | 0 docs/{ => archive}/agents/worker.md | 0 docs/{ => archive}/context-engineering.md | 0 docs/{ => archive}/crash-marking-fix.md | 0 docs/{ => archive}/deviation-rules.md | 0 docs/{ => archive}/execution-artifacts.md | 0 docs/{ => archive}/initiatives.md | 0 docs/{ => archive}/model-profiles.md | 0 docs/{ => archive}/session-state.md | 0 docs/{ => archive}/task-granularity.md | 0 docs/{ => archive}/tasks.md | 0 docs/{ => archive}/verification.md | 0 docs/{ => archive}/wireframes/agent-inbox.md | 0 .../wireframes/initiative-dashboard.md | 0 .../wireframes/initiative-detail.md | 0 docs/cli-config.md | 141 ++++++++++ docs/database.md | 172 ++++++++++++ docs/dispatch-events.md | 92 ++++++ docs/frontend.md | 134 +++++++++ docs/git-process-logging.md | 121 ++++++++ docs/server-api.md | 200 ++++++++++++++ docs/testing.md | 79 ++++++ drizzle/0019_add_execution_mode.sql | 1 + drizzle/0020_add_change_sets.sql | 25 ++ drizzle/0021_drop_proposals.sql | 2 + drizzle/meta/_journal.json | 21 ++ package-lock.json | 118 +++++++- packages/shared/src/index.ts | 2 +- packages/shared/src/types.ts | 5 +- packages/web/package.json | 1 + .../web/src/components/ChangeSetBanner.tsx | 139 ++++++++++ .../src/components/CreateInitiativeDialog.tsx | 39 +++ packages/web/src/components/ExecutionTab.tsx | 3 + .../web/src/components/InitiativeHeader.tsx | 22 +- packages/web/src/components/StatusBadge.tsx | 2 + .../editor/ContentProposalReview.tsx | 234 ---------------- .../components/editor/RefineAgentPanel.tsx | 18 +- .../components/execution/BreakdownSection.tsx | 26 +- .../components/execution/PhaseDetailPanel.tsx | 55 ++-- .../web/src/components/review/ReviewTab.tsx | 161 ++++++++--- packages/web/src/components/ui/select.tsx | 158 +++++++++++ packages/web/src/hooks/useRefineAgent.ts | 31 +-- packages/web/src/lib/invalidation.ts | 8 +- packages/web/src/routes/initiatives/$id.tsx | 3 + src/agent/completion-detection.test.ts | 8 +- src/agent/file-io.test.ts | 1 + src/agent/manager.ts | 20 +- src/agent/output-handler.test.ts | 17 -- src/agent/output-handler.ts | 261 +++++++++++++----- src/agent/process-manager.ts | 3 +- src/agent/types.ts | 6 + src/container.ts | 55 +++- .../conflict-resolution-service.ts | 35 ++- src/db/repositories/change-set-repository.ts | 36 +++ src/db/repositories/drizzle/change-set.ts | 110 ++++++++ src/db/repositories/drizzle/index.ts | 2 +- src/db/repositories/drizzle/proposal.ts | 133 --------- src/db/repositories/index.ts | 9 +- src/db/repositories/proposal-repository.ts | 35 --- src/db/schema.ts | 71 +++-- src/dispatch/manager.ts | 34 ++- src/dispatch/phase-manager.ts | 34 ++- src/events/index.ts | 3 + src/events/types.ts | 56 ++++ src/execution/orchestrator.ts | 226 +++++++++++++++ src/git/branch-manager.ts | 41 +++ src/git/branch-naming.ts | 42 +++ src/git/simple-git-branch-manager.ts | 107 +++++++ src/server/trpc-adapter.ts | 16 +- .../integration/crash-race-condition.test.ts | 6 +- src/trpc/context.ts | 20 +- src/trpc/router.ts | 4 +- src/trpc/routers/_helpers.ts | 32 ++- src/trpc/routers/change-set.ts | 146 ++++++++++ src/trpc/routers/initiative.ts | 5 + src/trpc/routers/phase.ts | 57 +++- src/trpc/routers/proposal.ts | 189 ------------- src/trpc/subscriptions.ts | 8 + 83 files changed, 3200 insertions(+), 913 deletions(-) create mode 100644 docs/agent.md create mode 100644 docs/architecture.md rename docs/{ => archive}/agent-lifecycle-refactor.md (100%) rename docs/{ => archive}/agents/architect.md (100%) rename docs/{ => archive}/agents/verifier.md (100%) rename docs/{ => archive}/agents/worker.md (100%) rename docs/{ => archive}/context-engineering.md (100%) rename docs/{ => archive}/crash-marking-fix.md (100%) rename docs/{ => archive}/deviation-rules.md (100%) rename docs/{ => archive}/execution-artifacts.md (100%) rename docs/{ => archive}/initiatives.md (100%) rename docs/{ => archive}/model-profiles.md (100%) rename docs/{ => archive}/session-state.md (100%) rename docs/{ => archive}/task-granularity.md (100%) rename docs/{ => archive}/tasks.md (100%) rename docs/{ => archive}/verification.md (100%) rename docs/{ => archive}/wireframes/agent-inbox.md (100%) rename docs/{ => archive}/wireframes/initiative-dashboard.md (100%) rename docs/{ => archive}/wireframes/initiative-detail.md (100%) create mode 100644 docs/cli-config.md create mode 100644 docs/database.md create mode 100644 docs/dispatch-events.md create mode 100644 docs/frontend.md create mode 100644 docs/git-process-logging.md create mode 100644 docs/server-api.md create mode 100644 docs/testing.md create mode 100644 drizzle/0019_add_execution_mode.sql create mode 100644 drizzle/0020_add_change_sets.sql create mode 100644 drizzle/0021_drop_proposals.sql create mode 100644 packages/web/src/components/ChangeSetBanner.tsx delete mode 100644 packages/web/src/components/editor/ContentProposalReview.tsx create mode 100644 packages/web/src/components/ui/select.tsx create mode 100644 src/db/repositories/change-set-repository.ts create mode 100644 src/db/repositories/drizzle/change-set.ts delete mode 100644 src/db/repositories/drizzle/proposal.ts delete mode 100644 src/db/repositories/proposal-repository.ts create mode 100644 src/execution/orchestrator.ts create mode 100644 src/git/branch-manager.ts create mode 100644 src/git/branch-naming.ts create mode 100644 src/git/simple-git-branch-manager.ts create mode 100644 src/trpc/routers/change-set.ts delete mode 100644 src/trpc/routers/proposal.ts diff --git a/CLAUDE.md b/CLAUDE.md index f104b0b..f82e750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,54 +1,50 @@ # Codewalk District -Multi-agent workspace for orchestrating multiple Claude Code agents. +Multi-agent workspace for orchestrating multiple AI coding agents working in parallel on a shared codebase. -## Database +**Architecture**: [docs/architecture.md](docs/architecture.md) — system diagram, module map, entity relationships, tech stack. -Schema is defined in `src/db/schema.ts` using drizzle-orm. Migrations are managed by drizzle-kit. +## Quick Reference -See [docs/database-migrations.md](docs/database-migrations.md) for the full migration workflow, rules, and commands. +| Module | Docs | Path | +|--------|------|------| +| Agent (lifecycle, providers, accounts) | [docs/agent.md](docs/agent.md) | `src/agent/` | +| Database (schema, repositories) | [docs/database.md](docs/database.md) | `src/db/` | +| Server & API (tRPC procedures) | [docs/server-api.md](docs/server-api.md) | `src/server/`, `src/trpc/`, `src/coordination/` | +| Frontend (React UI) | [docs/frontend.md](docs/frontend.md) | `packages/web/` | +| CLI & Config | [docs/cli-config.md](docs/cli-config.md) | `src/cli/`, `src/config/` | +| Dispatch & Events | [docs/dispatch-events.md](docs/dispatch-events.md) | `src/dispatch/`, `src/events/` | +| Git, Process, Logging | [docs/git-process-logging.md](docs/git-process-logging.md) | `src/git/`, `src/process/`, `src/logger/`, `src/logging/` | +| Testing | [docs/testing.md](docs/testing.md) | `src/test/` | +| Database Migrations | [docs/database-migrations.md](docs/database-migrations.md) | `drizzle/` | +| Logging Guide | [docs/logging.md](docs/logging.md) | `src/logger/` | -Key rule: **never use raw SQL for schema initialization.** Always use `drizzle-kit generate` and the migration system. +Pre-implementation design docs are archived in `docs/archive/`. -## Logging +## Key Rules -Structured logging via pino. See [docs/logging.md](docs/logging.md) for full details. - -Key rule: use `createModuleLogger()` from `src/logger/index.ts` for backend logging. Keep `console.log` for CLI user-facing output only. +- **Database**: Never use raw SQL for schema initialization. Use `drizzle-kit generate` and the migration system. See [docs/database-migrations.md](docs/database-migrations.md). +- **Logging**: Use `createModuleLogger()` from `src/logger/index.ts`. Keep `console.log` for CLI user-facing output only. +- **Hexagonal architecture**: Repository ports in `src/db/repositories/*.ts`, Drizzle adapters in `src/db/repositories/drizzle/*.ts`. All re-exported from `src/db/index.ts`. +- **tRPC context**: Optional repos accessed via `require*Repository()` helpers in `src/trpc/routers/_helpers.ts`. ## Build -After completing any change to server-side code (`src/**`), rebuild and re-link the `cw` binary: - ```sh npm run build && npm link ``` +Run after any change to server-side code (`src/**`). + ## Testing -### Unit Tests - ```sh -npm test +npm test # Unit tests +REAL_CLAUDE_TESTS=1 npm test -- src/test/integration/real-providers/ --test-timeout=300000 # Real provider tests (~$0.50) ``` -### E2E Tests (Real CLI) +See [docs/testing.md](docs/testing.md) for details. -Real provider integration tests call actual CLI tools and incur API costs. They are **skipped by default**. +## Documentation Maintenance -```sh -# Claude tests (~$0.50, ~3 min) -REAL_CLAUDE_TESTS=1 npm test -- src/test/integration/real-providers/ --test-timeout=300000 - -# Codex tests only -REAL_CODEX_TESTS=1 npm test -- src/test/integration/real-providers/codex-manager.test.ts --test-timeout=300000 - -# Both providers -REAL_CLAUDE_TESTS=1 REAL_CODEX_TESTS=1 npm test -- src/test/integration/real-providers/ --test-timeout=300000 -``` - -Test files in `src/test/integration/real-providers/`: -- `claude-manager.test.ts` - Spawn, output parsing, session resume -- `schema-retry.test.ts` - Schema validation, JSON extraction, retry logic -- `crash-recovery.test.ts` - Server restart simulation -- `codex-manager.test.ts` - Codex provider tests +**After every code change, update the relevant docs/ file.** Documentation must stay in sync with implementation. When adding a new module, table, tRPC procedure, component, or CLI command, update the corresponding doc. When refactoring, update affected docs and remove stale information. diff --git a/docs/agent.md b/docs/agent.md new file mode 100644 index 0000000..9bd9e56 --- /dev/null +++ b/docs/agent.md @@ -0,0 +1,103 @@ +# Agent Module + +`src/agent/` — Agent lifecycle management, output parsing, multi-provider support, and account failover. + +## File Inventory + +| File | Purpose | +|------|---------| +| `types.ts` | Core types: `AgentInfo`, `AgentManager` interface, `SpawnOptions`, `StreamEvent` | +| `manager.ts` | `MultiProviderAgentManager` — main orchestrator class | +| `process-manager.ts` | `AgentProcessManager` — worktree creation, command building, detached spawn | +| `output-handler.ts` | `OutputHandler` — JSONL stream parsing, completion detection, proposal creation | +| `file-tailer.ts` | `FileTailer` — watches output files, emits line events | +| `file-io.ts` | Input/output file I/O: frontmatter writing, signal.json reading, tiptap conversion | +| `markdown-to-tiptap.ts` | Markdown to Tiptap JSON conversion using MarkdownManager | +| `index.ts` | Public exports, `ClaudeAgentManager` deprecated alias | + +### Sub-modules + +| Directory | Purpose | +|-----------|---------| +| `providers/` | Provider registry, presets (7 providers), config types | +| `providers/parsers/` | Provider-specific output parsers (Claude JSONL, generic line) | +| `accounts/` | Account discovery, config dir setup, credential management, usage API | +| `credentials/` | `AccountCredentialManager` — credential injection per account | +| `lifecycle/` | `LifecycleController` — retry policy, signal recovery, missing signal instructions | +| `prompts/` | Mode-specific prompt builders (execute, discuss, breakdown, decompose, refine) | + +## Key Flows + +### Spawning an Agent + +1. **tRPC procedure** calls `agentManager.spawn(options)` +2. Manager generates alias (adjective-animal), creates DB record +3. `AgentProcessManager.createWorktree()` — creates git worktree at `.cw-worktrees/agent//` +4. `file-io.writeInputFiles()` — writes `.cw/input/` with initiative, pages, phase, task as frontmatter +5. Provider config builds spawn command via `buildSpawnCommand()` +6. `spawnDetached()` — launches detached child process with file output redirection +7. `FileTailer` watches output file, fires `onEvent` and `onRawContent` callbacks +8. `OutputHandler.handleStreamEvent()` processes each JSONL line +9. DB record updated with PID, output file path, session ID +10. `agent:spawned` event emitted + +### Completion Detection + +1. `FileTailer` detects process exit +2. `OutputHandler.handleCompletion()` triggered +3. **Primary path**: Reads `.cw/output/signal.json` from agent worktree +4. Signal contains `{ status: "done"|"questions"|"error", result?, questions?, error? }` +5. Agent DB status updated accordingly (idle, waiting_for_input, crashed) +6. For `done`: proposals created from structured output; `agent:stopped` emitted +7. For `questions`: parsed and stored as `pendingQuestions`; `agent:waiting` emitted +8. **Fallback**: If signal.json missing, lifecycle controller retries with instruction injection + +### Account Failover + +1. On usage-limit error, `markAccountExhausted(id, until)` called +2. `findNextAvailable(provider)` returns least-recently-used non-exhausted account +3. Agent re-spawned with new account's credentials +4. `agent:account_switched` event emitted + +### Resume Flow + +1. tRPC `resumeAgent` called with `answers: Record` +2. Manager looks up agent's session ID and provider config +3. `buildResumeCommand()` creates resume command with session flag +4. `formatAnswersAsPrompt(answers)` converts answers to prompt text +5. New detached process spawned, same worktree, incremented session number + +## Provider Configuration + +Providers defined in `providers/presets.ts`: + +| Provider | Command | Resume | Prompt Mode | +|----------|---------|--------|-------------| +| claude | `claude` | `--resume ` | native (`-p`) | +| claude-code | `claude` | `--resume ` | native | +| codex | `codex` | none | flag (`--prompt`) | +| aider | `aider` | none | flag (`--message`) | +| cline | `cline` | none | flag | +| continue | `continue` | none | flag | +| cursor-agent | `cursor` | none | flag | + +Each provider config specifies: `command`, `args`, `resumeStyle`, `promptMode`, `structuredOutput`, `sessionId` extraction, `nonInteractive` options. + +## Output Parsing + +The `OutputHandler` processes JSONL streams from Claude CLI: + +- `text_delta` events → accumulated as text output, emitted via `agent:output` +- `init` event → session ID extracted +- `result` event → final result with structured data +- Signal file (`signal.json`) → authoritative completion status + +For providers without structured output, the generic line parser accumulates raw text. + +## Log Chunks + +Agent output is persisted to `agent_log_chunks` table: +- `onRawContent` callback fires for every output chunk +- Fire-and-forget DB insert (no FK to agents — survives deletion) +- Session tracking: spawn=1, resume=previousMax+1 +- Read path concatenates all sessions for full output history diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a6ea205 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,109 @@ +# Architecture Overview + +Codewalk District is a multi-agent workspace that orchestrates multiple AI coding agents (Claude, Codex, etc.) working in parallel on a shared codebase. + +## System Diagram + +``` +CLI (cw) + └── CoordinationServer + ├── HTTP Server (node:http, port 3847) + │ ├── GET /health + │ ├── GET /status + │ └── POST /trpc/* → tRPC Router (68+ procedures) + ├── EventBus (typed pub/sub, 30+ event types) + ├── MultiProviderAgentManager + │ ├── ProcessManager (detached child processes) + │ ├── WorktreeManager (git worktrees per agent) + │ ├── OutputHandler (JSONL stream parsing) + │ ├── AccountManager (round-robin, exhaustion failover) + │ └── LifecycleController (retry, signal recovery) + ├── DispatchManager (task queue, dependency resolution) + ├── PhaseDispatchManager (phase queue, DAG ordering) + └── CoordinationManager (merge queue, conflict resolution) + +Web UI (packages/web/) + └── React 19 + TanStack Router + tRPC React Query + ├── Initiative management (CRUD, projects) + ├── Page editor (Tiptap rich text) + ├── Pipeline visualization (phase DAG) + ├── Execution tab (task dispatch, agent monitoring) + └── Review tab (proposal accept/dismiss) +``` + +## Architectural Patterns + +### Hexagonal Architecture (Ports & Adapters) +Every data-access layer follows this pattern: +- **Port** (interface): `src/db/repositories/-repository.ts` +- **Adapter** (implementation): `src/db/repositories/drizzle/-repository.ts` +- Re-exported from barrel files so consumers import from `src/db/` + +### Event-Driven Communication +All inter-module communication flows through a typed `EventBus`: +- Managers emit domain events (agent:spawned, task:completed, etc.) +- tRPC subscriptions bridge events to the frontend via SSE +- Dispatch/coordination reacts to task/phase lifecycle events + +### Data-Driven Provider Configuration +Agent providers (Claude, Codex, etc.) are defined as configuration objects, not code: +- `AgentProviderConfig` specifies command, args, resume style, prompt mode +- `buildSpawnCommand()` / `buildResumeCommand()` are generic and data-driven + +## Module Map + +| Module | Path | Purpose | Docs | +|--------|------|---------|------| +| Agent | `src/agent/` | Agent lifecycle, output parsing, accounts | [agent.md](agent.md) | +| Database | `src/db/` | Schema, repositories, migrations | [database.md](database.md) | +| Server & API | `src/server/`, `src/trpc/` | HTTP server, tRPC procedures | [server-api.md](server-api.md) | +| Frontend | `packages/web/` | React UI, components, hooks | [frontend.md](frontend.md) | +| CLI & Config | `src/cli/`, `src/config/` | CLI commands, workspace config | [cli-config.md](cli-config.md) | +| Dispatch | `src/dispatch/` | Task/phase queue and dispatch | [dispatch-events.md](dispatch-events.md) | +| Coordination | `src/coordination/` | Merge queue, conflict resolution | [server-api.md](server-api.md#coordination) | +| Git | `src/git/` | Worktree management, project clones | [git-process-logging.md](git-process-logging.md) | +| Process | `src/process/` | Child process spawn/track/stop | [git-process-logging.md](git-process-logging.md) | +| Logging | `src/logger/`, `src/logging/` | Structured logging, file capture | [git-process-logging.md](git-process-logging.md) | +| Events | `src/events/` | EventBus, typed event system | [dispatch-events.md](dispatch-events.md) | +| Shared | `packages/shared/` | Types shared between frontend/backend | [frontend.md](frontend.md) | +| Tests | `src/test/` | E2E, integration, fixtures | [testing.md](testing.md) | + +## Entity Relationships + +``` +INITIATIVES (root aggregate) + ├── N PHASES (with phase_dependencies DAG) + │ └── N TASKS + ├── N TASKS (initiative-level, with task_dependencies DAG) + │ └── N TASKS (parentTaskId self-ref for decomposition) + ├── N PAGES (parentPageId self-ref for hierarchy) + ├── N INITIATIVE_PROJECTS (junction → PROJECTS M:M) + └── N PROPOSALS + +AGENTS (independent aggregate) + ├── → TASK (optional) + ├── → INITIATIVE (optional) + ├── → ACCOUNT (optional) + ├── N PROPOSALS + └── N MESSAGES (sender/recipient) + +ACCOUNTS → N AGENTS +AGENT_LOG_CHUNKS (no FK, survives agent deletion) +``` + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Runtime | Node.js (ESM) | +| Language | TypeScript (strict) | +| Database | SQLite via better-sqlite3 + Drizzle ORM | +| HTTP | Native node:http | +| API | tRPC v11 | +| Frontend | React 19, TanStack Router, Tailwind CSS, shadcn/ui | +| Editor | Tiptap (ProseMirror) | +| Git | simple-git | +| Process | execa (detached) | +| Logging | pino (structured) | +| Testing | vitest | +| Build | tsup (server), Vite (frontend) | diff --git a/docs/agent-lifecycle-refactor.md b/docs/archive/agent-lifecycle-refactor.md similarity index 100% rename from docs/agent-lifecycle-refactor.md rename to docs/archive/agent-lifecycle-refactor.md diff --git a/docs/agents/architect.md b/docs/archive/agents/architect.md similarity index 100% rename from docs/agents/architect.md rename to docs/archive/agents/architect.md diff --git a/docs/agents/verifier.md b/docs/archive/agents/verifier.md similarity index 100% rename from docs/agents/verifier.md rename to docs/archive/agents/verifier.md diff --git a/docs/agents/worker.md b/docs/archive/agents/worker.md similarity index 100% rename from docs/agents/worker.md rename to docs/archive/agents/worker.md diff --git a/docs/context-engineering.md b/docs/archive/context-engineering.md similarity index 100% rename from docs/context-engineering.md rename to docs/archive/context-engineering.md diff --git a/docs/crash-marking-fix.md b/docs/archive/crash-marking-fix.md similarity index 100% rename from docs/crash-marking-fix.md rename to docs/archive/crash-marking-fix.md diff --git a/docs/deviation-rules.md b/docs/archive/deviation-rules.md similarity index 100% rename from docs/deviation-rules.md rename to docs/archive/deviation-rules.md diff --git a/docs/execution-artifacts.md b/docs/archive/execution-artifacts.md similarity index 100% rename from docs/execution-artifacts.md rename to docs/archive/execution-artifacts.md diff --git a/docs/initiatives.md b/docs/archive/initiatives.md similarity index 100% rename from docs/initiatives.md rename to docs/archive/initiatives.md diff --git a/docs/model-profiles.md b/docs/archive/model-profiles.md similarity index 100% rename from docs/model-profiles.md rename to docs/archive/model-profiles.md diff --git a/docs/session-state.md b/docs/archive/session-state.md similarity index 100% rename from docs/session-state.md rename to docs/archive/session-state.md diff --git a/docs/task-granularity.md b/docs/archive/task-granularity.md similarity index 100% rename from docs/task-granularity.md rename to docs/archive/task-granularity.md diff --git a/docs/tasks.md b/docs/archive/tasks.md similarity index 100% rename from docs/tasks.md rename to docs/archive/tasks.md diff --git a/docs/verification.md b/docs/archive/verification.md similarity index 100% rename from docs/verification.md rename to docs/archive/verification.md diff --git a/docs/wireframes/agent-inbox.md b/docs/archive/wireframes/agent-inbox.md similarity index 100% rename from docs/wireframes/agent-inbox.md rename to docs/archive/wireframes/agent-inbox.md diff --git a/docs/wireframes/initiative-dashboard.md b/docs/archive/wireframes/initiative-dashboard.md similarity index 100% rename from docs/wireframes/initiative-dashboard.md rename to docs/archive/wireframes/initiative-dashboard.md diff --git a/docs/wireframes/initiative-detail.md b/docs/archive/wireframes/initiative-detail.md similarity index 100% rename from docs/wireframes/initiative-detail.md rename to docs/archive/wireframes/initiative-detail.md diff --git a/docs/cli-config.md b/docs/cli-config.md new file mode 100644 index 0000000..c2ce326 --- /dev/null +++ b/docs/cli-config.md @@ -0,0 +1,141 @@ +# CLI & Configuration + +`src/cli/` — CLI commands, `src/config/` — workspace configuration, `src/bin/` — entry point. + +## Entry Point + +`src/bin/cw.ts` — hashbang entry that imports and runs the CLI. + +## CLI Framework + +Uses **Commander.js** for command parsing. + +## Global Flags + +- `-s, --server` — Start coordination server in foreground +- `-p, --port ` — Server port (default: 3847, env: `CW_PORT`) +- `-d, --debug` — Enable debug mode (archive agent workdirs before cleanup) + +## Commands + +### System +| Command | Description | +|---------|-------------| +| `cw init` | Create `.cwrc` workspace file in current directory | +| `cw start` | Start coordination server (see Server Wiring below) | +| `cw stop` | Stop running server (reads PID file, sends signal) | +| `cw status` | Query server health endpoint | +| `cw id [-n count]` | Generate nanoid(s) offline | + +### Agent Management (`cw agent`) +| Command | Description | +|---------|-------------| +| `spawn --task [--name] [--provider] [--initiative] [--cwd]` | Spawn agent | +| `stop ` | Stop running agent | +| `delete ` | Delete agent, clean workdir/branches/logs | +| `list` | List all agents with status | +| `get ` | Agent details (ID, task, session, worktree, status) | +| `resume ` | Resume with JSON answers or single string | +| `result ` | Get execution result | + +### Task Management (`cw task`) +| Command | Description | +|---------|-------------| +| `list --parent\|--phase\|--initiative ` | List tasks with counts | +| `get ` | Task details | +| `status ` | Update status | + +### Dispatch (`cw dispatch`) +| Command | Description | +|---------|-------------| +| `queue ` | Queue task for dispatch | +| `next` | Dispatch next available task | +| `status` | Show queue status | +| `complete ` | Mark task complete | + +### Initiative Management (`cw initiative`) +| Command | Description | +|---------|-------------| +| `create [--project ]` | Create initiative | +| `list [-s status]` | List initiatives | +| `get ` | Initiative details | +| `phases ` | List phases | + +### Architect (`cw architect`) +| Command | Description | +|---------|-------------| +| `discuss [-c context]` | Start discussion agent | +| `breakdown [-s summary]` | Start breakdown agent | +| `decompose [-t taskName] [-c context]` | Decompose phase into tasks | + +### Phase (`cw phase`) +| Command | Description | +|---------|-------------| +| `add-dependency --phase --depends-on ` | Add dependency edge | +| `dependencies ` | List dependencies | +| `queue ` | Queue approved phase | +| `dispatch` | Dispatch next phase | +| `queue-status` | Show phase queue | + +### Merge & Coordination (`cw merge`, `cw coordinate`) +| Command | Description | +|---------|-------------| +| `merge queue ` | Queue task for merge | +| `merge status` | Show merge queue | +| `merge next` | Show next mergeable task | +| `coordinate [-t branch]` | Process all ready merges (default: main) | + +### Messages (`cw message`) +| Command | Description | +|---------|-------------| +| `list [--agent ] [--status ]` | List messages | +| `read ` | Read full message | +| `respond ` | Respond to message | + +### Projects (`cw project`) +| Command | Description | +|---------|-------------| +| `register --name --url ` | Register git repo | +| `list` | List projects | +| `delete ` | Delete project | + +### Accounts (`cw account`) +| Command | Description | +|---------|-------------| +| `add [--provider] [--email]` | Auto-discover or manually register account | +| `list` | Show accounts with exhaustion status | +| `remove ` | Remove account | +| `refresh` | Clear expired exhaustion markers | + +## Server Wiring + +The CLI is the composition root. It creates concrete implementations and passes them as `contextDeps`: + +``` +ServerContextDeps = Omit +``` + +This includes all repositories, managers, and the credential manager. The server adds `eventBus`, `serverStartedAt`, and `processCount` at startup. + +## Workspace Configuration + +`src/config/` module: + +### .cwrc File +JSON file at workspace root that marks a `cw` workspace: +```json +{ "version": 1 } +``` + +### findWorkspaceRoot() +Walks up directory tree looking for `.cwrc` file. Returns the directory containing it, or throws if not found. + +### Schema +Currently `{ version: 1 }` — extend with new top-level keys as needed. + +### Files +| File | Purpose | +|------|---------| +| `types.ts` | CwrcConfig type definition | +| `cwrc.ts` | findWorkspaceRoot(), readCwrc(), writeCwrc() | +| `index.ts` | Public API exports | diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..04236af --- /dev/null +++ b/docs/database.md @@ -0,0 +1,172 @@ +# Database Module + +`src/db/` — SQLite database via better-sqlite3 + Drizzle ORM with hexagonal architecture. + +## Architecture + +- **Schema**: `src/db/schema.ts` — all tables, columns, relations +- **Ports** (interfaces): `src/db/repositories/*.ts` — 10 repository interfaces +- **Adapters** (implementations): `src/db/repositories/drizzle/*.ts` — 10 Drizzle adapters +- **Barrel exports**: `src/db/index.ts` re-exports everything + +All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes. + +## Tables + +### initiatives +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | nanoid | +| name | text NOT NULL | | +| status | text enum | 'active' \| 'completed' \| 'archived', default 'active' | +| mergeRequiresApproval | integer/boolean | default true | +| mergeTarget | text nullable | target branch for merges | +| createdAt, updatedAt | integer/timestamp | | + +### phases +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| initiativeId | text FK → initiatives (cascade) | | +| name | text NOT NULL | | +| content | text nullable | Tiptap JSON | +| status | text enum | 'pending' \| 'approved' \| 'in_progress' \| 'completed' \| 'blocked' | +| createdAt, updatedAt | integer/timestamp | | + +### phase_dependencies +`phaseId` FK → phases, `dependsOnPhaseId` FK → phases. Both cascade delete. + +### tasks +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| phaseId | text nullable FK → phases (cascade) | | +| initiativeId | text nullable FK → initiatives (cascade) | | +| parentTaskId | text nullable self-ref FK (cascade) | decomposition hierarchy | +| name | text NOT NULL | | +| description | text nullable | | +| type | text enum | 'auto' \| 'checkpoint:human-verify' \| 'checkpoint:decision' \| 'checkpoint:human-action' | +| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'breakdown' \| 'decompose' \| 'refine' \| 'verify' \| 'merge' \| 'review' | +| priority | text enum | 'low' \| 'medium' \| 'high' | +| status | text enum | 'pending_approval' \| 'pending' \| 'in_progress' \| 'completed' \| 'blocked' | +| requiresApproval | integer/boolean nullable | null = inherit from initiative | +| order | integer | default 0 | +| createdAt, updatedAt | integer/timestamp | | + +### task_dependencies +`taskId` FK → tasks, `dependsOnTaskId` FK → tasks. Both cascade delete. + +### agents +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| name | text NOT NULL UNIQUE | human-readable alias (adjective-animal) | +| taskId | text nullable FK → tasks (set null) | | +| initiativeId | text nullable FK → initiatives (set null) | | +| sessionId | text nullable | CLI session ID for resume | +| worktreeId | text NOT NULL | path to agent's git worktree | +| provider | text NOT NULL | default 'claude' | +| accountId | text nullable FK → accounts (set null) | | +| status | text enum | 'idle' \| 'running' \| 'waiting_for_input' \| 'stopped' \| 'crashed' | +| mode | text enum | 'execute' \| 'discuss' \| 'breakdown' \| 'decompose' \| 'refine' | +| pid | integer nullable | OS process ID | +| exitCode | integer nullable | | +| outputFilePath | text nullable | | +| result | text nullable | JSON | +| pendingQuestions | text nullable | JSON | +| userDismissedAt | integer/timestamp nullable | | +| createdAt, updatedAt | integer/timestamp | | + +### accounts +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| email | text NOT NULL | | +| provider | text NOT NULL | default 'claude' | +| configJson | text nullable | serialized .claude.json | +| credentials | text nullable | serialized .credentials.json | +| isExhausted | integer/boolean | default false | +| exhaustedUntil | integer/timestamp nullable | | +| lastUsedAt | integer/timestamp nullable | round-robin scheduling | +| sortOrder | integer | | +| createdAt, updatedAt | integer/timestamp | | + +### proposals +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| agentId | text FK → agents (cascade) | | +| initiativeId | text FK → initiatives (cascade) | | +| targetType | text enum | 'page' \| 'phase' \| 'task' | +| targetId | text nullable | existing entity ID, null for creates | +| title, summary, content | text | markdown body | +| metadata | text nullable | JSON | +| status | text enum | 'pending' \| 'accepted' \| 'dismissed' | +| sortOrder | integer | | +| createdAt, updatedAt | integer/timestamp | | + +### pages +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| initiativeId | text FK → initiatives (cascade) | | +| parentPageId | text nullable self-ref FK (cascade) | root page has NULL | +| title | text NOT NULL | | +| content | text nullable | Tiptap JSON | +| sortOrder | integer | | +| createdAt, updatedAt | integer/timestamp | | + +### projects +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| name | text NOT NULL UNIQUE | | +| url | text NOT NULL UNIQUE | git repo URL | +| createdAt, updatedAt | integer/timestamp | | + +### initiative_projects (junction) +`initiativeId` + `projectId` with unique index. Both FK cascade. + +### messages +Self-referencing (parentMessageId) for threading. Sender/recipient types: 'agent' | 'user'. + +### agent_log_chunks +| Column | Type | Notes | +|--------|------|-------| +| id | text PK | | +| agentId | text NOT NULL | **NO FK** — survives agent deletion | +| agentName | text NOT NULL | snapshot for display | +| sessionNumber | integer | spawn=1, resume=prev+1 | +| content | text NOT NULL | raw JSONL chunk | +| createdAt | integer/timestamp | | + +Index on `agentId` for fast queries. + +## Repository Interfaces + +10 repositories, each with standard CRUD plus domain-specific methods: + +| Repository | Key Methods | +|-----------|-------------| +| InitiativeRepository | create, findById, findAll, findByStatus, update, delete | +| PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId | +| TaskRepository | + findByParentTaskId, findByPhaseId, findPendingApproval, createDependency | +| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus | +| MessageRepository | + findPendingForUser, findRequiringResponse, findReplies | +| PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId | +| ProjectRepository | + junction ops: setInitiativeProjects (diff-based), findProjectsByInitiativeId | +| AccountRepository | + findNextAvailable (round-robin), markExhausted, clearExpiredExhaustion | +| ProposalRepository | + findByAgentIdAndStatus, updateManyByAgentId, countByAgentIdAndStatus | +| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount | + +## Migrations + +Located in `drizzle/`. Applied via `ensureSchema()` on startup using Drizzle's `migrate()`. + +Key rules: +- **Never use raw SQL** for schema initialization +- Run `npx drizzle-kit generate` to create migrations +- See [database-migrations.md](database-migrations.md) for full workflow +- Snapshots stale after 0008; migrations 0008+ are hand-written + +Current migrations: 0000 through 0018 (19 total). diff --git a/docs/dispatch-events.md b/docs/dispatch-events.md new file mode 100644 index 0000000..c40abea --- /dev/null +++ b/docs/dispatch-events.md @@ -0,0 +1,92 @@ +# Dispatch & Events + +`src/dispatch/` — Task and phase dispatch queues. `src/events/` — Typed event bus. + +## Event Bus + +`src/events/` — Typed pub/sub system for inter-module communication. + +### Architecture +- **Port**: `EventBus` interface with `emit(event)` and `on(type, handler)` +- **Adapter**: `TypedEventBus` using Node.js `EventEmitter` +- All events implement `BaseEvent { type, timestamp, payload }` + +### Event Types (48) + +| Category | Events | Count | +|----------|--------|-------| +| **Agent** | `agent:spawned`, `agent:stopped`, `agent:crashed`, `agent:resumed`, `agent:account_switched`, `agent:deleted`, `agent:waiting`, `agent:output` | 8 | +| **Task** | `task:queued`, `task:dispatched`, `task:completed`, `task:blocked`, `task:pending_approval` | 5 | +| **Phase** | `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked` | 4 | +| **Merge** | `merge:queued`, `merge:started`, `merge:completed`, `merge:conflicted` | 4 | +| **Page** | `page:created`, `page:updated`, `page:deleted` | 3 | +| **Process** | `process:spawned`, `process:stopped`, `process:crashed` | 3 | +| **Server** | `server:started`, `server:stopped` | 2 | +| **Worktree** | `worktree:created`, `worktree:removed`, `worktree:merged`, `worktree:conflict` | 4 | +| **Account** | `account:credentials_refreshed`, `account:credentials_expired`, `account:credentials_validated` | 3 | +| **Log** | `log:entry` | 1 | + +### Key Event Payloads + +```typescript +AgentSpawnedEvent { agentId, name, taskId, worktreeId, provider } +AgentStoppedEvent { agentId, name, taskId, reason } + // reason: 'user_requested'|'task_complete'|'error'|'waiting_for_input'| + // 'context_complete'|'breakdown_complete'|'decompose_complete'|'refine_complete' +AgentWaitingEvent { agentId, name, taskId, sessionId, questions[] } +AgentOutputEvent { agentId, stream, data } +TaskCompletedEvent { taskId, agentId, success, message } +TaskQueuedEvent { taskId, priority, dependsOn[] } +PhaseStartedEvent { phaseId, initiativeId } +MergeConflictedEvent { taskId, agentId, worktreeId, targetBranch, conflictingFiles[] } +AccountCredentialsRefreshedEvent { accountId, expiresAt, previousExpiresAt? } +``` + +## Task Dispatch + +`src/dispatch/` — In-memory queue with dependency-ordered dispatch. + +### Architecture +- **Port**: `DispatchManager` interface +- **Adapter**: `DefaultDispatchManager` + +### How Task Dispatch Works + +1. **Queue** — `queue(taskId)` fetches task dependencies, adds to internal Map +2. **Dispatch** — `dispatchNext()` finds highest-priority task with all deps complete +3. **Priority order**: high > medium > low, then oldest first (FIFO within priority) +4. **Checkpoint skip** — Tasks with type starting with `checkpoint:` skip auto-dispatch +5. **Approval check** — `completeTask()` checks `requiresApproval` (task-level, then initiative-level) +6. **Approval flow** — If approval required: status → `pending_approval`, emit `task:pending_approval` + +### DispatchManager Methods + +| Method | Purpose | +|--------|---------| +| `queue(taskId)` | Add task to dispatch queue | +| `dispatchNext()` | Find and dispatch next ready task | +| `getNextDispatchable()` | Get next task without dispatching | +| `completeTask(taskId, agentId?)` | Complete with approval check | +| `approveTask(taskId)` | Approve pending task | +| `blockTask(taskId, reason)` | Block task with reason | +| `getQueueState()` | Return queued, ready, blocked tasks | + +## Phase Dispatch + +`DefaultPhaseDispatchManager` — Same pattern for phases: + +1. **Queue** — `queuePhase(phaseId)` validates phase is approved, gets dependencies +2. **Dispatch** — `dispatchNextPhase()` finds phase with all deps complete +3. **Auto-queue tasks** — When phase starts, all pending tasks are queued +4. **Events** — `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked` + +### PhaseDispatchManager Methods + +| Method | Purpose | +|--------|---------| +| `queuePhase(phaseId)` | Queue approved phase | +| `dispatchNextPhase()` | Start next ready phase, auto-queue its tasks | +| `getNextDispatchablePhase()` | Get next phase without dispatching | +| `completePhase(phaseId)` | Mark phase complete | +| `blockPhase(phaseId, reason)` | Block phase | +| `getPhaseQueueState()` | Return queued, ready, blocked phases | diff --git a/docs/frontend.md b/docs/frontend.md new file mode 100644 index 0000000..58e9cbe --- /dev/null +++ b/docs/frontend.md @@ -0,0 +1,134 @@ +# Frontend + +`packages/web/` — React web UI for managing initiatives, agents, and content. + +## Tech Stack + +| Technology | Purpose | +|-----------|---------| +| React 19 | UI framework | +| TanStack Router | File-based routing | +| tRPC React Query | Type-safe API client with caching | +| Tailwind CSS | Utility-first styling | +| shadcn/ui | Component library (button, card, dialog, dropdown, input, label, textarea, badge, sonner) | +| Tiptap | Rich text editor (ProseMirror-based) | +| Lucide | Icon library | + +## Path Alias + +`@/*` maps to `./src/*` (configured in `tsconfig.app.json`). + +## Routes + +| Route | Component | Purpose | +|-------|-----------|---------| +| `/` | `routes/index.tsx` | Dashboard / initiative list | +| `/initiatives/$id` | `routes/initiatives/$initiativeId.tsx` | Initiative detail (tabbed) | +| `/settings` | `routes/settings/index.tsx` | Settings page | + +## Initiative Detail Tabs + +The initiative detail page has three tabs managed via local state (not URL params): + +1. **Content Tab** — Page tree + Tiptap editor, proposal review +2. **Execution Tab** — Pipeline visualization, phase management, task dispatch +3. **Review Tab** — Pending proposals from agents + +## Component Inventory (73 components) + +### Core Components (`src/components/`) +| Component | Purpose | +|-----------|---------| +| `InitiativeHeader` | Initiative name, project badges, merge config | +| `InitiativeContent` | Content tab with page tree + editor | +| `StatusBadge` | Colored status indicator | +| `TaskRow` | Task list item with status, priority, category | +| `QuestionForm` | Agent question form with options | +| `InboxDetailPanel` | Agent message detail + response form | +| `ProjectPicker` | Checkbox list for project selection | +| `RegisterProjectDialog` | Dialog to register new git project | +| `Skeleton` | Loading placeholder | + +### Editor Components (`src/components/editor/`) +| Component | Purpose | +|-----------|---------| +| `TiptapEditor` | Core rich text editor wrapper | +| `PageEditor` | Page content editor with auto-save | +| `PhaseContentEditor` | Phase content editor | +| `ContentProposalReview` | Accept/dismiss proposals from agents | +| `SlashCommandMenu` | Slash command popup in editor | + +### Execution Components (`src/components/execution/`) +| Component | Purpose | +|-----------|---------| +| `ExecutionTab` | Main execution view container | +| `ExecutionContext` | React context for execution state | +| `PhaseDetailPanel` | Phase detail with tasks, dependencies, breakdown | +| `PhaseSidebar` | Phase list sidebar | +| `TaskDetailPanel` | Task detail with agent status, output | + +### Pipeline Components (`src/components/pipeline/`) +| Component | Purpose | +|-----------|---------| +| `PipelineVisualization` | DAG visualization of phase pipeline | +| `PipelineNode` | Individual phase node in pipeline | +| `PipelineEdge` | Dependency edge between nodes | + +### Review Components (`src/components/review/`) +| Component | Purpose | +|-----------|---------| +| `ReviewTab` | Review tab container | +| `ProposalCard` | Individual proposal display | + +### UI Primitives (`src/components/ui/`) +shadcn/ui components: badge, button, card, dialog, dropdown-menu, input, label, sonner, textarea. + +## Custom Hooks (`src/hooks/`) + +| Hook | Purpose | +|------|---------| +| `useRefineAgent` | Manages refine agent lifecycle for initiative | +| `useDecomposeAgent` | Manages decompose agent for phase breakdown | +| `useAgentOutput` | Subscribes to live agent output stream | + +## tRPC Client + +Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query for caching and optimistic updates. + +## Key User Flows + +### Creating an Initiative +1. Dashboard → "New Initiative" → enter name, select projects +2. `createInitiative` mutation → auto-creates root page +3. Navigate to initiative detail + +### Managing Content (Pages) +1. Content tab → page tree sidebar +2. Click page → Tiptap editor loads content +3. Edit → auto-saves via `updatePage` mutation +4. Use slash commands for formatting + +### Refining Content with AI +1. Content tab → "Refine" button +2. `spawnArchitectRefine` mutation → agent analyzes pages +3. Agent creates proposals (page edits, new phases, tasks) +4. Proposals appear in review section → accept/dismiss each + +### Pipeline Visualization +1. Execution tab → pipeline DAG shows phases as nodes +2. Drag to add dependencies between phases +3. Approve phases → queue for dispatch +4. Tasks auto-queued when phase starts + +### Decomposing Phases +1. Select phase → "Breakdown" button +2. `spawnArchitectDecompose` mutation → agent creates task proposals +3. Accept proposals → tasks created under phase +4. View tasks in phase detail panel + +## Shared Package + +`packages/shared/` exports: +- `sortByPriorityAndQueueTime()` — priority-based task sorting +- `topologicalSort()` / `groupByPipelineColumn()` — phase DAG layout +- Shared type re-exports from `packages/shared/src/types.ts` diff --git a/docs/git-process-logging.md b/docs/git-process-logging.md new file mode 100644 index 0000000..e9e23cd --- /dev/null +++ b/docs/git-process-logging.md @@ -0,0 +1,121 @@ +# Git, Process, and Logging Modules + +Three infrastructure modules supporting agent execution. + +## Git Module (`src/git/`) + +Manages git worktrees for isolated agent workspaces. + +### Architecture +- **Port**: `WorktreeManager` interface +- **Adapter**: `SimpleGitWorktreeManager` using simple-git library + +### WorktreeManager Methods + +| Method | Purpose | +|--------|---------| +| `create(id, branch, baseBranch?)` | Create worktree with new branch (default base: 'main') | +| `remove(id)` | Clean up worktree directory | +| `list()` | All worktrees including main | +| `get(id)` | Specific worktree by ID | +| `diff(id)` | Changed files vs HEAD | +| `merge(id, targetBranch)` | Merge worktree branch into target | + +### Worktree Storage +Worktrees stored in `.cw-worktrees/` subdirectory of the repo. Each agent gets a worktree at `.cw-worktrees/agent//`. + +### Merge Flow +1. Check out target branch +2. `git merge --no-edit` +3. On success: emit `worktree:merged` +4. On conflict: `git merge --abort`, emit `worktree:conflict` with conflicting file list +5. Restore original branch + +### Project Clones +- `cloneProject(url, destPath)` — Simple git clone wrapper +- `ensureProjectClone(project, workspaceRoot)` — Idempotent: checks if clone exists, clones if not +- `getProjectCloneDir(name, id)` — Canonical path: `repos/-/` + +### Events Emitted +`worktree:created`, `worktree:removed`, `worktree:merged`, `worktree:conflict` + +--- + +## Process Module (`src/process/`) + +Spawns, tracks, and controls child processes. + +### Classes + +**ProcessRegistry** — In-memory metadata store: +- `register(info)`, `unregister(id)`, `get(id)`, `getAll()`, `getByPid(pid)`, `updateStatus(id, status)` + +**ProcessManager** — Lifecycle management: + +| Method | Purpose | +|--------|---------| +| `spawn(options)` | Spawn detached process (survives parent exit) | +| `stop(id)` | SIGTERM → wait 5s → SIGKILL | +| `stopAll()` | Stop all running processes in parallel | +| `restart(id)` | Stop + re-spawn with same options | +| `isRunning(id)` | Check with `process.kill(pid, 0)` | + +### Spawn Details +- Uses `execa` with `detached: true`, `stdio: 'ignore'` +- Calls `subprocess.unref()` so parent can exit +- Exit handler updates registry and emits events + +### Events Emitted +`process:spawned`, `process:stopped`, `process:crashed` + +--- + +## Logger Module (`src/logger/`) + +Structured logging via **pino**. + +### Usage +```typescript +import { createModuleLogger } from './logger/index.js'; +const log = createModuleLogger('my-module'); +log.info({ key: 'value' }, 'message'); +``` + +### Configuration +| Env Var | Effect | +|---------|--------| +| `CW_LOG_LEVEL` | Override log level | +| `CW_LOG_PRETTY` | Set to `'1'` for human-readable output | +| `NODE_ENV=development` | Default to 'debug' level | + +### Output +- Default: JSON to stderr (fd 2) +- Pretty mode: pino-pretty to stdout with colors and timestamps + +--- + +## Logging Module (`src/logging/`) + +File-based per-process output capture (separate from pino). + +### Classes + +**LogManager** — Directory management: +- Base dir: `~/.cw/logs/` +- Structure: `{processId}/stdout.log`, `{processId}/stderr.log` +- `cleanOldLogs(retainDays)` — removes old directories by mtime + +**ProcessLogWriter** — File I/O with timestamps: +- `open()` — create directories and append-mode WriteStreams +- `writeStdout(data)` / `writeStderr(data)` — prefix each line with `[YYYY-MM-DD HH:mm:ss.SSS]` +- Handles backpressure (waits for drain event) +- Emits `log:entry` event via EventBus + +### Factory +```typescript +import { createLogger } from './logging/index.js'; +const writer = createLogger(processId, eventBus); +await writer.open(); +await writer.writeStdout('output data'); +await writer.close(); +``` diff --git a/docs/server-api.md b/docs/server-api.md new file mode 100644 index 0000000..e341e5c --- /dev/null +++ b/docs/server-api.md @@ -0,0 +1,200 @@ +# Server & API Module + +`src/server/` — HTTP server, `src/trpc/` — tRPC procedures, `src/coordination/` — merge queue. + +## HTTP Server + +**Framework**: Native `node:http` (no Express/Fastify) +**Default**: `127.0.0.1:3847` +**PID file**: `~/.cw/server.pid` + +### Routes + +| Route | Method | Purpose | +|-------|--------|---------| +| `/health` | GET | Health check (`{ status, uptime, processCount }`) | +| `/status` | GET | Full server status with process list | +| `/trpc/*` | POST | All tRPC procedure calls | + +### Lifecycle +- `CoordinationServer.start()` — checks PID file, creates HTTP server, emits `server:started` +- `CoordinationServer.stop()` — emits `server:stopped`, closes server, removes PID file +- `GracefulShutdown` handles SIGTERM/SIGINT/SIGHUP with 10s timeout + +### tRPC Adapter +`trpc-adapter.ts` converts `node:http` IncomingMessage/ServerResponse to fetch Request/Response for tRPC. Subscriptions stream via ReadableStream bodies (SSE). + +## tRPC Context + +All procedures share a context with optional dependencies: + +```typescript +interface TRPCContext { + eventBus: EventBus // always present + serverStartedAt: Date | null + processCount: number + agentManager?: AgentManager // optional + taskRepository?: TaskRepository + // ... all 10 repositories, 3 managers, credentialManager, workspaceRoot +} +``` + +Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTERNAL_SERVER_ERROR)` if a dependency is missing. + +## Procedure Reference + +### System +| Procedure | Type | Description | +|-----------|------|-------------| +| health | query | Health check with uptime | +| status | query | Server status with process list | +| systemHealthCheck | query | Account, agent, project health | + +### Agents +| Procedure | Type | Description | +|-----------|------|-------------| +| spawnAgent | mutation | Spawn new agent (taskId, prompt, provider, mode) | +| stopAgent | mutation | Stop agent by name or ID | +| deleteAgent | mutation | Delete agent and clean up worktree | +| dismissAgent | mutation | Dismiss agent (set userDismissedAt) | +| resumeAgent | mutation | Resume with answers | +| listAgents | query | All agents | +| getAgent | query | Single agent by name or ID | +| getAgentResult | query | Execution result | +| getAgentQuestions | query | Pending questions | +| getAgentOutput | query | Full output (DB chunks or file fallback) | +| getActiveRefineAgent | query | Active refine agent for initiative | +| listWaitingAgents | query | Agents waiting for input | +| onAgentOutput | subscription | Live output stream + buffered history | + +### Tasks +| Procedure | Type | Description | +|-----------|------|-------------| +| listTasks | query | Child tasks of parent | +| getTask | query | Single task | +| updateTaskStatus | mutation | Change task status | +| createInitiativeTask | mutation | Create task on initiative | +| createPhaseTask | mutation | Create task on phase | +| listInitiativeTasks | query | All tasks for initiative | +| listPhaseTasks | query | All tasks for phase | +| listPendingApprovals | query | Tasks with status=pending_approval | +| approveTask | mutation | Approve and complete task | + +### Initiatives +| Procedure | Type | Description | +|-----------|------|-------------| +| createInitiative | mutation | Create with optional projectIds, auto-creates root page | +| listInitiatives | query | Filter by status | +| getInitiative | query | With projects array | +| updateInitiative | mutation | Name, status | +| updateInitiativeMergeConfig | mutation | mergeRequiresApproval, mergeTarget | + +### Phases +| Procedure | Type | Description | +|-----------|------|-------------| +| createPhase | mutation | Create in initiative | +| listPhases | query | By initiative | +| getPhase | query | Single phase | +| updatePhase | mutation | Name, content, status | +| approvePhase | mutation | Validate and approve | +| deletePhase | mutation | Cascade delete | +| createPhasesFromBreakdown | mutation | Bulk create from agent output | +| createPhaseDependency | mutation | Add dependency edge | +| removePhaseDependency | mutation | Remove dependency edge | +| listInitiativePhaseDependencies | query | All dependency edges | +| getPhaseDependencies | query | What this phase depends on | +| getPhaseDependents | query | What depends on this phase | + +### Phase Dispatch +| Procedure | Type | Description | +|-----------|------|-------------| +| queuePhase | mutation | Queue approved phase | +| dispatchNextPhase | mutation | Start next ready phase | +| getPhaseQueueState | query | Queue state | +| createChildTasks | mutation | Create tasks from decompose parent | + +### Architect (High-Level Agent Spawning) +| Procedure | Type | Description | +|-----------|------|-------------| +| spawnArchitectDiscuss | mutation | Discussion agent | +| spawnArchitectBreakdown | mutation | Breakdown agent (generates phases) | +| spawnArchitectRefine | mutation | Refine agent (generates proposals) | +| spawnArchitectDecompose | mutation | Decompose agent (generates tasks) | + +### Dispatch +| Procedure | Type | Description | +|-----------|------|-------------| +| queueTask | mutation | Add task to dispatch queue | +| dispatchNext | mutation | Dispatch next ready task | +| getQueueState | query | Queue state | +| completeTask | mutation | Complete with approval check | + +### Coordination (Merge Queue) +| Procedure | Type | Description | +|-----------|------|-------------| +| queueMerge | mutation | Queue task for merge | +| processMerges | mutation | Process merge queue | +| getMergeQueueStatus | query | Queue state | +| getNextMergeable | query | Next ready-to-merge task | + +### Projects +| Procedure | Type | Description | +|-----------|------|-------------| +| registerProject | mutation | Clone git repo, create record | +| listProjects | query | All projects | +| getProject | query | Single project | +| deleteProject | mutation | Delete clone and record | +| getInitiativeProjects | query | Projects for initiative | +| updateInitiativeProjects | mutation | Sync junction table | + +### Pages +| Procedure | Type | Description | +|-----------|------|-------------| +| getRootPage | query | Auto-creates if missing | +| getPage | query | Single page | +| getPageUpdatedAtMap | query | Bulk updatedAt check | +| listPages | query | By initiative | +| listChildPages | query | By parent page | +| createPage | mutation | Create, emit page:created | +| updatePage | mutation | Title/content/sortOrder, emit page:updated | +| deletePage | mutation | Delete, emit page:deleted | + +### Accounts +| Procedure | Type | Description | +|-----------|------|-------------| +| listAccounts | query | All accounts | +| addAccount | mutation | Create account | +| removeAccount | mutation | Delete account | +| refreshAccounts | mutation | Clear expired exhaustion | +| updateAccountAuth | mutation | Update credentials | +| markAccountExhausted | mutation | Set exhaustion timer | +| listProviderNames | query | Available provider names | + +### Proposals +| Procedure | Type | Description | +|-----------|------|-------------| +| listProposals | query | By agent or initiative | +| acceptProposal | mutation | Apply side effects, auto-dismiss agent | +| dismissProposal | mutation | Dismiss, auto-dismiss agent | +| acceptAllProposals | mutation | Bulk accept with error collection | +| dismissAllProposals | mutation | Bulk dismiss | + +### Subscriptions (SSE) +| Procedure | Type | Events | +|-----------|------|--------| +| onEvent | subscription | All 30+ event types | +| onAgentUpdate | subscription | agent:* events (8 types) | +| onTaskUpdate | subscription | task:* + phase:* events (8 types) | +| onPageUpdate | subscription | page:created/updated/deleted | + +Subscriptions use `eventBusIterable()` — queue-based async generator, max 1000 events, 30s heartbeat. + +## Coordination Module + +`src/coordination/` manages merge queue: + +- **CoordinationManager** port: `queueMerge`, `getNextMergeable`, `processMerges`, `handleConflict`, `getQueueState` +- **DefaultCoordinationManager** adapter: in-memory queue, dependency-ordered processing +- **ConflictResolutionService**: creates resolution tasks for merge conflicts +- Merge flow: queue → check deps → merge via WorktreeManager → handle conflicts +- Events: `merge:queued`, `merge:started`, `merge:completed`, `merge:conflicted` diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..9d484d7 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,79 @@ +# Testing + +`src/test/` — Test infrastructure, fixtures, and test suites. + +## Framework + +**vitest** (Vite-native test runner) + +## Test Categories + +### Unit Tests +Located alongside source files (`*.test.ts`): +- `src/agent/*.test.ts` — Manager, output handler, completion detection, file I/O, process manager +- `src/db/repositories/drizzle/*.test.ts` — Repository adapters +- `src/dispatch/*.test.ts` — Dispatch manager +- `src/git/manager.test.ts` — Worktree operations +- `src/process/*.test.ts` — Process registry and manager +- `src/logging/*.test.ts` — Log manager and writer + +### E2E Tests (Mocked Agents) +`src/test/e2e/`: +| File | Scenarios | +|------|-----------| +| `happy-path.test.ts` | Single task, parallel, complex flows | +| `architect-workflow.test.ts` | Discussion + breakdown agent workflows | +| `decompose-workflow.test.ts` | Task decomposition with child tasks | +| `phase-dispatch.test.ts` | Phase-level dispatch with dependencies | +| `recovery-scenarios.test.ts` | Crash recovery, agent resume | +| `edge-cases.test.ts` | Boundary conditions | +| `extended-scenarios.test.ts` | Advanced multi-phase workflows | + +### Integration Tests (Real Providers) +`src/test/integration/real-providers/` — **skipped by default** (cost real money): +| File | Provider | Cost | +|------|----------|------| +| `claude-manager.test.ts` | Claude CLI | ~$0.10 | +| `codex-manager.test.ts` | Codex | varies | +| `schema-retry.test.ts` | Claude CLI | ~$0.10 | +| `crash-recovery.test.ts` | Claude CLI | ~$0.10 | + +Enable with env vars: `REAL_CLAUDE_TESTS=1`, `REAL_CODEX_TESTS=1` + +## Test Infrastructure + +### TestHarness (`src/test/harness.ts`) +Central test utility providing: +- In-memory SQLite database with schema applied +- All 10 repository instances +- `MockAgentManager` — simulates agent behavior (done, questions, error) +- `MockWorktreeManager` — in-memory worktree simulator +- `CapturingEventBus` — captures events for assertions +- `DefaultDispatchManager` and `DefaultPhaseDispatchManager` +- 25+ helper methods for test scenarios + +### Fixtures (`src/test/fixtures.ts`) +Pre-built task hierarchies for testing: +| Fixture | Structure | +|---------|-----------| +| `SIMPLE_FIXTURE` | 1 initiative → 1 phase → 1 group → 3 tasks (A→B, A→C deps) | +| `PARALLEL_FIXTURE` | 1 initiative → 1 phase → 2 groups → 4 independent tasks | +| `COMPLEX_FIXTURE` | 1 initiative → 2 phases → 4 groups → cross-phase dependencies | + +### Real Provider Harness (`src/test/integration/real-providers/harness.ts`) +- Creates real database, real agent manager with real CLI tools +- Provides `describeRealClaude()` / `describeRealCodex()` that skip when env var not set +- `MINIMAL_PROMPTS` — cheap prompts for testing output parsing + +## Running Tests + +```sh +# Unit tests +npm test + +# Specific test file +npm test -- src/agent/manager.test.ts + +# Real provider tests (costs money!) +REAL_CLAUDE_TESTS=1 npm test -- src/test/integration/real-providers/ --test-timeout=300000 +``` diff --git a/drizzle/0019_add_execution_mode.sql b/drizzle/0019_add_execution_mode.sql new file mode 100644 index 0000000..dd684e9 --- /dev/null +++ b/drizzle/0019_add_execution_mode.sql @@ -0,0 +1 @@ +ALTER TABLE initiatives ADD COLUMN execution_mode TEXT NOT NULL DEFAULT 'review_per_phase'; diff --git a/drizzle/0020_add_change_sets.sql b/drizzle/0020_add_change_sets.sql new file mode 100644 index 0000000..1567a69 --- /dev/null +++ b/drizzle/0020_add_change_sets.sql @@ -0,0 +1,25 @@ +-- Add change_sets and change_set_entries tables +CREATE TABLE `change_sets` ( + `id` text PRIMARY KEY NOT NULL, + `agent_id` text REFERENCES `agents`(`id`) ON DELETE SET NULL, + `agent_name` text NOT NULL, + `initiative_id` text NOT NULL REFERENCES `initiatives`(`id`) ON DELETE CASCADE, + `mode` text NOT NULL, + `summary` text, + `status` text NOT NULL DEFAULT 'applied', + `reverted_at` integer, + `created_at` integer NOT NULL +);--> statement-breakpoint +CREATE TABLE `change_set_entries` ( + `id` text PRIMARY KEY NOT NULL, + `change_set_id` text NOT NULL REFERENCES `change_sets`(`id`) ON DELETE CASCADE, + `entity_type` text NOT NULL, + `entity_id` text NOT NULL, + `action` text NOT NULL, + `previous_state` text, + `new_state` text, + `sort_order` integer NOT NULL DEFAULT 0, + `created_at` integer NOT NULL +);--> statement-breakpoint +CREATE INDEX `change_sets_initiative_id_idx` ON `change_sets`(`initiative_id`);--> statement-breakpoint +CREATE INDEX `change_set_entries_change_set_id_idx` ON `change_set_entries`(`change_set_id`); diff --git a/drizzle/0021_drop_proposals.sql b/drizzle/0021_drop_proposals.sql new file mode 100644 index 0000000..3a681d0 --- /dev/null +++ b/drizzle/0021_drop_proposals.sql @@ -0,0 +1,2 @@ +-- Drop proposals table (replaced by change_sets + change_set_entries) +DROP TABLE IF EXISTS `proposals`; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index ee88eaf..2264f0e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -134,6 +134,27 @@ "when": 1771113600000, "tag": "0018_drop_phase_number", "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1771200000000, + "tag": "0019_add_execution_mode", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1771286400000, + "tag": "0020_add_change_sets", + "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1771372800000, + "tag": "0021_drop_proposals", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6f9c29d..c2449d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1297,7 +1297,6 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", - "devOptional": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" @@ -1307,7 +1306,6 @@ "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", - "devOptional": true, "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.4", @@ -1318,7 +1316,6 @@ "version": "2.1.7", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.5" @@ -1332,7 +1329,6 @@ "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@isaacs/balanced-match": { @@ -1477,6 +1473,12 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -1487,7 +1489,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -1511,7 +1512,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1538,7 +1538,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -1641,7 +1640,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1878,7 +1876,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -2028,6 +2025,67 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -2131,11 +2189,25 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -2154,7 +2226,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2169,11 +2240,33 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "dev": true, "license": "MIT" }, "node_modules/@remirror/core-constants": { @@ -8016,6 +8109,7 @@ "@codewalk-district/shared": "*", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@tanstack/react-query": "^5.75.0", "@tanstack/react-router": "^1.158.0", "@tiptap/extension-link": "^3.19.0", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index b6720a2..b3ff90a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,3 @@ export type { AppRouter } from './trpc.js'; -export type { Initiative, Phase, Task, Agent, Message, PendingQuestions, QuestionItem, SubscriptionEvent, Project, Proposal } from './types.js'; +export type { Initiative, Phase, Task, Agent, Message, PendingQuestions, QuestionItem, SubscriptionEvent, Project, ChangeSet, ChangeSetEntry } from './types.js'; export { sortByPriorityAndQueueTime, topologicalSortPhases, groupPhasesByDependencyLevel, type SortableItem, type PhaseForSort, type DependencyEdge, type PipelineColumn } from './utils.js'; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 393659d..89642a1 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,6 +1,9 @@ -export type { Initiative, Phase, Task, Agent, Message, Page, Project, Account, Proposal } from '../../../src/db/schema.js'; +export type { Initiative, Phase, Task, Agent, Message, Page, Project, Account, ChangeSet, ChangeSetEntry } from '../../../src/db/schema.js'; export type { PendingQuestions, QuestionItem } from '../../../src/agent/types.js'; +export type ExecutionMode = 'yolo' | 'review_per_phase'; +export type PhaseStatus = 'pending' | 'approved' | 'in_progress' | 'completed' | 'blocked' | 'pending_review'; + /** * Shape of events received from tRPC subscription streams. * Used by the frontend in onData callbacks. diff --git a/packages/web/package.json b/packages/web/package.json index 219a352..7a5c529 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -13,6 +13,7 @@ "@codewalk-district/shared": "*", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@tanstack/react-query": "^5.75.0", "@tanstack/react-router": "^1.158.0", "@tiptap/extension-link": "^3.19.0", diff --git a/packages/web/src/components/ChangeSetBanner.tsx b/packages/web/src/components/ChangeSetBanner.tsx new file mode 100644 index 0000000..7c43e2c --- /dev/null +++ b/packages/web/src/components/ChangeSetBanner.tsx @@ -0,0 +1,139 @@ +import { useState, useCallback } from "react"; +import { ChevronDown, ChevronRight, Undo2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { trpc } from "@/lib/trpc"; +import type { ChangeSet } from "@codewalk-district/shared"; + +interface ChangeSetBannerProps { + changeSet: ChangeSet; + onDismiss: () => void; +} + +const MODE_LABELS: Record = { + breakdown: "phases", + decompose: "tasks", + refine: "pages", +}; + +export function ChangeSetBanner({ changeSet, onDismiss }: ChangeSetBannerProps) { + const [expanded, setExpanded] = useState(false); + const [conflicts, setConflicts] = useState(null); + + const detailQuery = trpc.getChangeSet.useQuery( + { id: changeSet.id }, + { enabled: expanded }, + ); + + const revertMutation = trpc.revertChangeSet.useMutation({ + onSuccess: (result) => { + if (!result.success && "conflicts" in result) { + setConflicts(result.conflicts); + } else { + setConflicts(null); + } + }, + }); + + const handleRevert = useCallback( + (force?: boolean) => { + revertMutation.mutate({ id: changeSet.id, force }); + }, + [changeSet.id, revertMutation], + ); + + const entries = detailQuery.data?.entries ?? []; + const entityLabel = MODE_LABELS[changeSet.mode] ?? "entities"; + const isReverted = changeSet.status === "reverted"; + + return ( +
+
+
+ + {isReverted && ( + + (reverted) + + )} +
+
+ {!isReverted && ( + + )} + +
+
+ + {conflicts && ( +
+

+ Conflicts detected: +

+
    + {conflicts.map((c, i) => ( +
  • {c}
  • + ))} +
+ +
+ )} + + {expanded && ( +
+ {detailQuery.isLoading &&

Loading entries...

} + {entries.map((entry) => ( +
+ + {entry.action === "create" ? "+" : entry.action === "delete" ? "-" : "~"} + + + {entry.entityType} + {entry.newState && (() => { + try { + const parsed = JSON.parse(entry.newState); + return parsed.name || parsed.title ? `: ${parsed.name || parsed.title}` : ""; + } catch { return ""; } + })()} + +
+ ))} + {entries.length === 0 && !detailQuery.isLoading && ( +

No entries

+ )} +
+ )} +
+ ); +} diff --git a/packages/web/src/components/CreateInitiativeDialog.tsx b/packages/web/src/components/CreateInitiativeDialog.tsx index 17c4c9c..0d70a2c 100644 --- a/packages/web/src/components/CreateInitiativeDialog.tsx +++ b/packages/web/src/components/CreateInitiativeDialog.tsx @@ -10,6 +10,13 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { ProjectPicker } from "./ProjectPicker"; @@ -25,6 +32,8 @@ export function CreateInitiativeDialog({ }: CreateInitiativeDialogProps) { const [name, setName] = useState(""); const [projectIds, setProjectIds] = useState([]); + const [executionMode, setExecutionMode] = useState<"yolo" | "review_per_phase">("review_per_phase"); + const [mergeTarget, setMergeTarget] = useState(""); const [error, setError] = useState(null); const utils = trpc.useUtils(); @@ -63,6 +72,8 @@ export function CreateInitiativeDialog({ if (open) { setName(""); setProjectIds([]); + setExecutionMode("review_per_phase"); + setMergeTarget(""); setError(null); } }, [open]); @@ -73,6 +84,8 @@ export function CreateInitiativeDialog({ createMutation.mutate({ name: name.trim(), projectIds: projectIds.length > 0 ? projectIds : undefined, + executionMode, + mergeTarget: mergeTarget.trim() || null, }); } @@ -98,6 +111,32 @@ export function CreateInitiativeDialog({ autoFocus /> +
+ + +
+
+ + setMergeTarget(e.target.value)} + /> +