diff --git a/.archon/scripts/echo-args.js b/.archon/scripts/echo-args.js new file mode 100644 index 0000000000..140a9ae4c9 --- /dev/null +++ b/.archon/scripts/echo-args.js @@ -0,0 +1,3 @@ +// Simple script node test — echoes input as JSON +const input = process.argv[2] ?? 'no-input'; +console.log(JSON.stringify({ echoed: input, timestamp: new Date().toISOString() })); diff --git a/.archon/workflows/e2e-all-nodes.yaml b/.archon/workflows/e2e-all-nodes.yaml new file mode 100644 index 0000000000..a3962b9740 --- /dev/null +++ b/.archon/workflows/e2e-all-nodes.yaml @@ -0,0 +1,51 @@ +# E2E smoke test — all node types +# Verifies: bash, prompt, script, structured output, model override, $nodeId.output refs +name: e2e-all-nodes +description: "Comprehensive E2E test exercising bash, prompt, script, and structured output nodes." +provider: claude + +nodes: + # 1. Bash node — no AI, runs shell, stdout captured as output + - id: bash-check + bash: "echo '{\"status\":\"ok\",\"cwd\":\"'$(pwd)'\"}'" + + # 2. Prompt node — simple AI call, verifies sendQuery works + - id: prompt-simple + prompt: "The bash node returned: $bash-check.output — confirm you received it by saying 'received'. Say nothing else." + depends_on: [bash-check] + + # 3. Prompt with model override — verifies model selection + - id: prompt-haiku + prompt: "Say 'haiku-ok' and nothing else." + model: haiku + depends_on: [bash-check] + + # 4. Structured output node — verifies output_format translation + - id: structured + prompt: "Classify the text 'hello world' as either 'greeting' or 'math'." + output_format: + type: object + properties: + category: + type: string + enum: ["greeting", "math"] + required: ["category"] + additionalProperties: false + depends_on: [prompt-simple] + + # 5. Bash node using $nodeId.output from structured node + - id: bash-read-output + bash: "echo 'Structured output category: $structured.output'" + depends_on: [structured] + + # 6. Script node (bun runtime) — verifies script execution + - id: script-echo + script: echo-args + runtime: bun + depends_on: [bash-check] + + # 7. Prompt with effort control — verifies effort passes through to SDK + - id: prompt-effort + prompt: "Say 'effort-ok' and nothing else." + effort: low + depends_on: [bash-check] diff --git a/.archon/workflows/e2e-claude-smoke.yaml b/.archon/workflows/e2e-claude-smoke.yaml new file mode 100644 index 0000000000..e4b0f776a4 --- /dev/null +++ b/.archon/workflows/e2e-claude-smoke.yaml @@ -0,0 +1,23 @@ +# E2E smoke test — Claude provider +# Verifies: provider selection, sendQuery, structured output, tool use +name: e2e-claude-smoke +description: "E2E smoke test for Claude provider. Runs a simple prompt + structured output node." +provider: claude + +nodes: + - id: simple + prompt: "What is 2+2? Answer with just the number, nothing else." + + - id: structured + prompt: "Classify this input as 'math' or 'text': '2+2=4'" + output_format: + type: object + properties: + category: + type: string + enum: ["math", "text"] + depends_on: [simple] + + - id: tool-use + prompt: "Read the file packages/providers/package.json and tell me the package name. Answer with just the name." + depends_on: [simple] diff --git a/.archon/workflows/e2e-codex-smoke.yaml b/.archon/workflows/e2e-codex-smoke.yaml new file mode 100644 index 0000000000..6650f92215 --- /dev/null +++ b/.archon/workflows/e2e-codex-smoke.yaml @@ -0,0 +1,21 @@ +# E2E smoke test — Codex provider +# Verifies: provider selection, sendQuery, structured output +name: e2e-codex-smoke +description: "E2E smoke test for Codex provider. Runs a simple prompt + structured output node." +provider: codex + +nodes: + - id: simple + prompt: "What is 2+2? Answer with just the number, nothing else." + + - id: structured + prompt: "Classify this input as 'math' or 'text': '2+2=4'. Return JSON only." + output_format: + type: object + properties: + category: + type: string + enum: ["math", "text"] + required: ["category"] + additionalProperties: false + depends_on: [simple] diff --git a/.archon/workflows/e2e-mixed-providers.yaml b/.archon/workflows/e2e-mixed-providers.yaml new file mode 100644 index 0000000000..6922056e50 --- /dev/null +++ b/.archon/workflows/e2e-mixed-providers.yaml @@ -0,0 +1,27 @@ +# E2E smoke test — mixed providers (Claude + Codex in same workflow) +# Verifies: per-node provider override, cross-provider $nodeId.output refs +name: e2e-mixed-providers +description: "Tests Claude and Codex providers in the same workflow with cross-provider output refs." + +# Default provider is claude +provider: claude + +nodes: + # 1. Claude node — default provider + - id: claude-node + prompt: "Say 'claude-ok' and nothing else." + + # 2. Codex node — provider override + - id: codex-node + prompt: "Say 'codex-ok' and nothing else." + provider: codex + + # 3. Claude node reads Codex output — cross-provider ref + - id: claude-reads-codex + prompt: "The codex node said: '$codex-node.output'. Confirm you received it by saying 'cross-provider-ok'. Say nothing else." + depends_on: [codex-node] + + # 4. Bash node verifies both outputs + - id: verify + bash: "echo 'claude=$claude-node.output codex=$codex-node.output cross=$claude-reads-codex.output'" + depends_on: [claude-node, codex-node, claude-reads-codex] diff --git a/.claude/commands/plan-feature.md b/.claude/commands/plan-feature.md index d4562e0f84..c3a12c4eab 100644 --- a/.claude/commands/plan-feature.md +++ b/.claude/commands/plan-feature.md @@ -23,7 +23,7 @@ Restate the feature request in your own words. Identify: 3. **Scope boundaries** — What is explicitly in scope vs. out of scope? 4. **Package impact** — Which of the 8 packages are affected? (`paths`, `git`, `isolation`, `workflows`, `core`, `adapters`, `server`, `web`) -5. **Interface changes** — Does this touch `IPlatformAdapter`, `IAssistantClient`, +5. **Interface changes** — Does this touch `IPlatformAdapter`, `IAgentProvider`, `IDatabase`, or `IWorkflowStore`? New interfaces needed? --- @@ -85,7 +85,7 @@ Before writing tasks, reason through: **Interface design:** - Prefer extending existing narrow interfaces over creating fat ones. - New interface methods only if they have a concrete current caller. -- Avoid adding methods to `IPlatformAdapter` or `IAssistantClient` unless essential. +- Avoid adding methods to `IPlatformAdapter` or `IAgentProvider` unless essential. **Test isolation strategy:** - `mock.module()` is process-global and permanent in Bun — plan test file placement carefully. diff --git a/.claude/commands/prime-backend.md b/.claude/commands/prime-backend.md index e2ff9dafee..7c34a3bee7 100644 --- a/.claude/commands/prime-backend.md +++ b/.claude/commands/prime-backend.md @@ -39,11 +39,11 @@ Read `packages/core/src/state/session-transitions.ts` in full — `TransitionTri ### 5. Understand AI Client Patterns -List clients: -!`ls packages/core/src/clients/` +List providers: +!`ls packages/core/src/providers/` -Read `packages/core/src/clients/factory.ts` for provider selection logic. -Read `packages/core/src/clients/claude.ts` first 50 lines — `IAssistantClient` implementation +Read `packages/core/src/providers/factory.ts` for provider selection logic. +Read `packages/core/src/providers/claude.ts` first 50 lines — `IAgentProvider` implementation with streaming event loop pattern. ### 6. Understand Database Layer @@ -52,7 +52,7 @@ List DB modules: !`ls packages/core/src/db/` Read `packages/core/src/types/index.ts` (or the main types file) first 60 lines for key -interfaces: `IPlatformAdapter`, `IAssistantClient`, `Conversation`, `Session`. +interfaces: `IPlatformAdapter`, `IAgentProvider`, `Conversation`, `Session`. ### 7. Understand the Server @@ -81,9 +81,9 @@ Summarize (under 250 words): - `TransitionTrigger` values and their behaviors - Only `plan-to-execute` immediately creates a new session; others deactivate first -### AI Clients -- `ClaudeClient` (claude-agent-sdk) and `CodexClient` (codex-sdk) -- `IAssistantClient` streaming pattern: `for await (const event of events)` +### AI Providers +- `ClaudeProvider` (claude-agent-sdk) and `CodexProvider` (codex-sdk) +- `IAgentProvider` streaming pattern: `for await (const event of events)` ### Key Database Tables - conversations, sessions, codebases, isolation_environments, workflow_runs, workflow_events, messages diff --git a/.claude/commands/prime-workflows.md b/.claude/commands/prime-workflows.md index 25509de48f..464d8f2e67 100644 --- a/.claude/commands/prime-workflows.md +++ b/.claude/commands/prime-workflows.md @@ -51,7 +51,7 @@ bridges these to SSE via `WorkflowEventBridge`. ### 7. Understand Dependency Injection Read `packages/workflows/src/deps.ts` — `WorkflowDeps` type: `IWorkflowPlatform`, -`IWorkflowAssistantClient`, `IWorkflowStore` injected at runtime. No direct DB or AI imports +`IWorkflowAgentProvider`, `IWorkflowStore` injected at runtime. No direct DB or AI imports inside this package. ### 8. See What Workflows Are Available diff --git a/.claude/commands/prime.md b/.claude/commands/prime.md index 50e5f45b4c..0a70ebe35f 100644 --- a/.claude/commands/prime.md +++ b/.claude/commands/prime.md @@ -64,8 +64,8 @@ Provide a concise summary (under 300 words) covering: ### Architecture - Package dependency order and each package's responsibility -- Key interfaces: `IPlatformAdapter`, `IAssistantClient`, `IDatabase`, `IWorkflowStore` -- Message flow: platform adapter → orchestrator-agent → command handler OR AI client +- Key interfaces: `IPlatformAdapter`, `IAgentProvider`, `IDatabase`, `IWorkflowStore` +- Message flow: platform adapter → orchestrator-agent → command handler OR AI provider - Workflow execution: `discoverWorkflows` → router → `executeWorkflow` (steps / loop / DAG) ### Current State diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md index 7e86a0dae4..658bc00def 100644 --- a/.claude/commands/validate.md +++ b/.claude/commands/validate.md @@ -21,7 +21,7 @@ Runs `tsc --noEmit` across all 8 packages via `bun --filter '*' type-check`. **What to look for:** - Missing return types (explicit return types required on all functions) -- Incorrect interface implementations (`IPlatformAdapter`, `IAssistantClient`, etc.) +- Incorrect interface implementations (`IPlatformAdapter`, `IAgentProvider`, etc.) - Import type errors (use `import type` for type-only imports) - Package boundary violations (e.g., `@archon/workflows` importing from `@archon/core`) diff --git a/.claude/docs/architecture-deep-dive.md b/.claude/docs/architecture-deep-dive.md index f5126d6fb4..d5e542b59b 100644 --- a/.claude/docs/architecture-deep-dive.md +++ b/.claude/docs/architecture-deep-dive.md @@ -33,7 +33,7 @@ Slack event → Otherwise → buildOrchestratorPrompt() (prompt-builder.ts:116) → Prompt includes: registered projects, discovered workflows, /invoke-workflow format → sessionDb.getActiveSession() → transitionSession('first-message') if none (orchestrator-agent.ts:462) - → getAssistantClient(conversation.ai_assistant_type) (orchestrator-agent.ts:470) + → getAgentProvider(conversation.ai_assistant_type) (orchestrator-agent.ts:470) → cwd = getArchonWorkspacesPath() (orchestrator-agent.ts:458) → handleBatchMode() or handleStreamMode() based on getStreamingMode() @@ -313,7 +313,7 @@ Narrows `IPlatformAdapter` to `WebAdapter` for web-specific methods: `setConvers | Message entry | `adapters/src/chat/slack/adapter.ts`, `server/src/index.ts` | | Orchestration | `core/src/orchestrator/orchestrator-agent.ts`, `core/src/orchestrator/orchestrator.ts` | | Locking | `core/src/utils/conversation-lock.ts` | -| AI clients | `core/src/clients/claude.ts`, `core/src/clients/factory.ts` | +| AI providers | `core/src/providers/claude.ts`, `core/src/providers/factory.ts` | | Commands | `core/src/handlers/command-handler.ts` | | Sessions | `core/src/db/sessions.ts`, `core/src/state/session-transitions.ts` | | Workflows | `workflows/src/executor.ts`, `workflows/src/dag-executor.ts`, `workflows/src/loader.ts` | diff --git a/.claude/rules/adapters.md b/.claude/rules/adapters.md deleted file mode 100644 index d49e683378..0000000000 --- a/.claude/rules/adapters.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -paths: - - "packages/adapters/**/*.ts" ---- - -# Adapters Conventions - -## Key Patterns - -- **Auth is inside adapters** — every adapter checks authorization before calling `onMessage()`. Silent rejection (no error response), log with masked user ID: `userId.slice(0, 4) + '***'`. -- **Whitelist parsing in constructor** — parse env var (`SLACK_ALLOWED_USER_IDS`, `TELEGRAM_ALLOWED_USER_IDS`, `GITHUB_ALLOWED_USERS`) using a co-located `parseAllowedUserIds()` / `parseAllowedUsers()` function. Empty list = open access. -- **Lazy logger pattern** — ALL adapter files use a module-level `cachedLog` + `getLog()` getter so test mocks intercept `createLogger` before the logger is instantiated. Never initialize logger at module scope. -- **Two handler patterns** (both valid): - - **Chat adapters** (Slack, Telegram, Discord): `onMessage(handler)` — adapter owns the event loop (polling/WebSocket), fires registered callback. Lock manager lives in the server's callback closure. Errors handled by caller via `createMessageErrorHandler`. - - **Forge adapters** (GitHub, Gitea): `handleWebhook(payload, signature)` — server HTTP route calls directly, returns 200 immediately. Full pipeline inside adapter (signature verification, repo cloning, command loading, context building). Lock manager injected in constructor. Errors caught internally and posted to issue/PR. -- **Message splitting** — use shared `splitIntoParagraphChunks(message, maxLength)` from `../../utils/message-splitting`. Two-pass: paragraph breaks first, then line breaks. Limits: Slack 12000, Telegram 4096, GitHub 65000. -- **`ensureThread()` is often a no-op** — Slack returns the same ID (already encoded as `channel:ts`), Telegram has no threads, GitHub issues are inherently threaded. - -## Conversation ID Formats - -| Platform | Format | Example | -|----------|--------|---------| -| Slack | `channel:thread_ts` | `C123ABC:1234567890.123456` | -| Telegram | numeric chat ID as string | `"1234567890"` | -| GitHub | `owner/repo#number` | `"acme/api#42"` | -| Web | user-provided string | `"my-chat"` | -| Discord | channel ID string | `"987654321098765432"` | - -## Architecture - -- All chat adapters implement `IPlatformAdapter` from `@archon/core` -- GitHub adapter is webhook-based (no polling); Slack/Telegram/Discord use polling -- GitHub adapter holds its own `ConversationLockManager` (injected in constructor) -- Slack conversation ID encodes both channel and thread: `sendMessage()` splits on `:` to extract `thread_ts` -- GitHub adapter adds `` marker to prevent self-triggering loops -- GitHub only responds to `issue_comment.created` events — NOT `issues.opened` / `pull_request.opened` (descriptions contain documentation, not commands; see #96) - -## Anti-patterns - -- Never put auth logic outside the adapter (no auth middleware in server routes) -- Never throw from `onMessage` handlers; errors surface to the caller -- Never call `sendMessage()` with a raw token or credential string in the message -- Never use the generic `exec` — always use `execFileAsync` for subprocess calls -- Never add a new adapter method to `IPlatformAdapter` unless ALL adapters need it; use optional methods (`sendStructuredEvent?`) for platform-specific capabilities diff --git a/.claude/rules/cli.md b/.claude/rules/cli.md deleted file mode 100644 index a954b6bd18..0000000000 --- a/.claude/rules/cli.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -paths: - - "packages/cli/**/*.ts" ---- - -# CLI Conventions - -## Commands - -```bash -# Workflow commands (require git repo) -bun run cli workflow list [--json] -bun run cli workflow run [message] [--branch ] [--from-branch ] [--no-worktree] [--resume] -bun run cli workflow status [runId] - -# Isolation commands -bun run cli isolation list -bun run cli isolation cleanup [days] # default: 7 days -bun run cli isolation cleanup --merged # removes merged branches + remote refs -bun run cli complete [--force] # full lifecycle: worktree + local/remote branches - -# Interactive -bun run cli chat [--cwd ] - -# Setup -bun run cli setup -bun run cli version -``` - -## Startup Behavior - -1. `@archon/paths/strip-cwd-env-boot` (first import) removes all Bun-auto-loaded CWD `.env` keys from `process.env` -2. Loads `~/.archon/.env` with `override: true` (Archon config wins over shell-inherited vars) -3. Smart Claude auth default: if no `CLAUDE_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`, sets `CLAUDE_USE_GLOBAL_AUTH=true` -4. Imports all commands AFTER dotenv setup - -## WorkflowRunOptions Interface - -```typescript -interface WorkflowRunOptions { - branchName?: string; // Explicit branch name for the worktree - fromBranch?: string; // Override base branch (start-point for worktree) - noWorktree?: boolean; // Opt out of isolation, run in live checkout - resume?: boolean; // Reuse worktree from last failed run -} -``` - -**Default behavior**: Creates worktree with auto-generated branch name (`archon/task-{workflow}-{timestamp}`). - -**Mutually exclusive** (enforced in both `cli.ts` pre-flight and `workflowRunCommand`): -- `--branch` + `--no-worktree` -- `--from` + `--no-worktree` -- `--resume` + `--branch` - -- `--branch feature-auth` → creates/reuses worktree for that branch -- (no flags) → creates worktree with auto-generated `archon/task-*` branch (isolation by default) -- `--no-worktree` → runs directly in live checkout (opt-out of isolation) -- `--from dev` → overrides the start-point for new worktree (works with or without `--branch`) -- `--resume` → resumes last run for this conversation (mutually exclusive with `--branch`) - -## Git Repo Requirement - -Workflow and isolation commands resolve CWD to the git repo root. Run from within a git repository (subdirectories work). The CLI calls `git rev-parse --show-toplevel` to find the root. - -## Conversation ID Format - -CLI generates: `cli-{timestamp}-{random6}` (e.g., `cli-1703123456789-a7f3bc`) - -## Port Allocation - -Worktree-aware: same hash-based algorithm as server (3190–4089 range). Running `bun dev` in a worktree auto-allocates a unique port. Same worktree always gets same port. - -## CLIAdapter - -The `CLIAdapter` implements `IPlatformAdapter`. It streams output to stdout. `getStreamingMode()` defaults to `'batch'` (configurable via constructor options). No auth needed — CLI is local only. - -## Architecture - -- `@archon/cli` depends on `@archon/core`, `@archon/workflows`, `@archon/git`, `@archon/isolation`, `@archon/paths` -- Uses `createWorkflowDeps()` from `@archon/core/workflows/store-adapter` to build workflow deps -- Database shared with server (same `~/.archon/archon.db` or `DATABASE_URL`) -- Conversation lifecycle: create → run workflow → persist messages (same DB as web UI) - -## Anti-patterns - -- Never run CLI commands without being inside a git repository (workflow/isolation commands will fail) -- Never set `DATABASE_URL` in `~/.archon/.env` to point at a target app's database -- Never use `--force` on `complete` unless branch is truly safe to delete (skips uncommitted check) -- Never add interactive prompts inside CLI commands — use flags for all options (non-interactive tool) diff --git a/.claude/rules/database.md b/.claude/rules/database.md deleted file mode 100644 index 0f579cc1a2..0000000000 --- a/.claude/rules/database.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -paths: - - "packages/core/src/db/**/*.ts" - - "migrations/**/*.sql" ---- - -# Database Conventions - -## 7 Tables (all prefixed `remote_agent_`) - -| Table | Purpose | -|-------|---------| -| `remote_agent_conversations` | Platform conversations, soft-delete (`deleted_at`), title, `hidden` flag | -| `remote_agent_sessions` | AI SDK sessions with `parent_session_id` audit chain, `transition_reason` | -| `remote_agent_codebases` | Repository metadata, `commands` JSONB | -| `remote_agent_isolation_environments` | Git worktree tracking, `workflow_type`, `workflow_id` | -| `remote_agent_workflow_runs` | Execution state, `working_path`, `last_activity_at` | -| `remote_agent_workflow_events` | Step-level event log per run | -| `remote_agent_messages` | Conversation history, tool call metadata as JSONB | - -## IDatabase Interface - -Auto-detects at startup: PostgreSQL if `DATABASE_URL` set, SQLite (`~/.archon/archon.db`) otherwise. - -```typescript -import { pool, getDialect } from './connection'; // pool = IDatabase instance - -// $1, $2 placeholders work for both PostgreSQL and SQLite -const result = await pool.query( - 'SELECT * FROM remote_agent_conversations WHERE id = $1', - [id] -); -const row = result.rows[0]; // rows is readonly T[] -``` - -Use `getDialect()` for dialect-specific expressions: `dialect.generateUuid()`, `dialect.now()`, `dialect.jsonMerge(col, paramIdx)`, `dialect.jsonArrayContains(col, path, paramIdx)`, `dialect.nowMinusDays(paramIdx)`. - -## Import Pattern — Namespaced Exports - -```typescript -// Use namespace imports for DB modules (consistent project-wide pattern) -import * as conversationDb from '@archon/core/db/conversations'; -import * as sessionDb from '@archon/core/db/sessions'; -import * as codebaseDb from '@archon/core/db/codebases'; -import * as workflowDb from '@archon/core/db/workflows'; -import * as messageDb from '@archon/core/db/messages'; -``` - -## INSERT Error Handling - -```typescript -try { - const result = await pool.query('INSERT INTO remote_agent_conversations ...', params); - return result.rows[0]; -} catch (error) { - log.error({ err: error, params }, 'db_insert_failed'); - throw new Error('Failed to create conversation'); -} -``` - -## UPDATE with rowCount Verification - -`updateConversation()` and similar throw `ConversationNotFoundError` / `SessionNotFoundError` when `rowCount === 0`. Callers must handle: - -```typescript -try { - await db.updateConversation(conversationId, { codebase_id: codebaseId }); -} catch (error) { - if (error instanceof ConversationNotFoundError) { - // Handle missing conversation specifically - } - throw error; // Re-throw unexpected errors -} -``` - -## Session Audit Trail - -Sessions are immutable. Every new session links back: `parent_session_id` → previous session, `transition_reason: TransitionTrigger`. Query the chain to understand history. `active = true` means the current session. - -## Soft Delete - -Conversations use soft-delete: `deleted_at IS NULL` filter should be included in all user-facing queries. `hidden = true` conversations are worker conversations (background workflows) — excluded from UI listings. - -## Anti-patterns - -- Never `SELECT *` in production queries on large tables — select specific columns -- Never write raw SQL strings in application code outside `packages/core/src/db/` modules -- Never bypass the `IDatabase` interface to call database drivers directly from other packages -- Never assume `rows[0]` exists without null-checking — queries can return empty arrays -- Never use `RETURNING *` in UPDATE when only checking success — check `rowCount` instead diff --git a/.claude/rules/dx-quirks.md b/.claude/rules/dx-quirks.md deleted file mode 100644 index 3d05e1f843..0000000000 --- a/.claude/rules/dx-quirks.md +++ /dev/null @@ -1,22 +0,0 @@ -# DX Quirks - -## Bun Log Elision - -When running `bun dev` from repo root, `--filter` truncates logs to `[N lines elided]`. -To see full logs: `cd packages/server && bun --watch src/index.ts` or `bun --cwd packages/server run dev`. - -## mock.module() Pollution - -`mock.module()` is process-global and irreversible — `mock.restore()` does NOT undo it. -Never add `afterAll(() => mock.restore())` for `mock.module()` cleanup. -Use `spyOn()` for internal modules (spy.mockRestore() DOES work). -When adding tests with `mock.module()`, ensure package.json runs it in a separate `bun test` invocation. - -## Worktree Port Allocation - -Worktrees auto-allocate ports (3190-4089 range, hash-based on path). Same worktree always gets same port. -Main repo defaults to 3090. Override: `PORT=4000 bun dev`. - -## bun run test vs bun test - -NEVER run `bun test` from repo root — it discovers all test files across packages in one process, causing ~135 mock pollution failures. Always use `bun run test` (which uses `bun --filter '*' test` for per-package isolation). diff --git a/.claude/rules/isolation-patterns.md b/.claude/rules/isolation-patterns.md deleted file mode 100644 index 0e763e03a2..0000000000 --- a/.claude/rules/isolation-patterns.md +++ /dev/null @@ -1,40 +0,0 @@ -# Isolation Architecture Patterns - -## Core Design - -- ALL isolation logic is centralized in the orchestrator — adapters are thin -- Every @mention auto-creates a worktree (simplicity > efficiency; worktrees are cheap) -- Data model is work-centric (`isolation_environments` table), enabling cross-platform sharing -- Cleanup is a separate service using git-first checks - -## Directory Structure - -``` -~/.archon/workspaces/owner/repo/ -├── source/ # Clone or symlink to local path -├── worktrees/ # Git worktrees for this project -├── artifacts/ # Workflow artifacts (NEVER in git) -│ ├── runs/{id}/ # Per-run artifacts ($ARTIFACTS_DIR) -│ └── uploads/{convId}/ # Web UI file uploads (ephemeral) -└── logs/ # Workflow execution logs -``` - -## Resolution Flow - -1. Adapter provides `IsolationHints` (conversationId, workflowId, branch preference) -2. Orchestrator's `validateAndResolveIsolation()` resolves hints → environment -3. WorktreeProvider creates worktree if needed, syncs with origin first -4. Environment tracked in `isolation_environments` table - -## Key Packages - -- `@archon/isolation` (`packages/isolation/src/`) — types, providers, resolver, error classifiers -- `@archon/git` (`packages/git/src/`) — branch, worktree, repo operations -- `@archon/paths` (`packages/paths/src/`) — path resolution utilities - -## Safety Rules - -- NEVER run `git clean -fd` — permanently deletes untracked files -- Use `classifyIsolationError()` to map git errors to user-friendly messages -- Trust git's natural guardrails (refuse to remove worktree with uncommitted changes) -- Use `execFileAsync` (not `exec`) when calling git directly diff --git a/.claude/rules/isolation.md b/.claude/rules/isolation.md deleted file mode 100644 index 1b849e7eca..0000000000 --- a/.claude/rules/isolation.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -paths: - - "packages/isolation/**/*.ts" - - "packages/git/**/*.ts" ---- - -# Isolation & Git Conventions - -## Branded Types (packages/git/src/types.ts) - -Always use the branded constructors — they reject empty strings at runtime and prevent passing the wrong string type: - -```typescript -import { toRepoPath, toBranchName, toWorktreePath } from '@archon/git'; -import type { RepoPath, BranchName, WorktreePath } from '@archon/git'; - -const repo = toRepoPath('/home/user/owner/repo'); // RepoPath -const branch = toBranchName('feature-auth'); // BranchName -const wt = toWorktreePath('/home/.archon/worktrees/x'); // WorktreePath -``` - -Git operations return `GitResult` discriminated union: `{ ok: true; value: T }` or `{ ok: false; error: GitError }`. Always check `.ok` before accessing `.value`. - -## IsolationResolver — 7-Step Resolution Order - -1. **Existing env** — use `existingEnvId` if worktree still exists on disk -2. **No codebase** — skip isolation entirely, return `status: 'none'` -3. **Workflow reuse** — find active env with same `(codebaseId, workflowType, workflowId)` -4. **Linked issue sharing** — PR can reuse the worktree from a linked issue -5. **PR branch adoption** — find existing worktree by branch name (`findWorktreeByBranch`) -6. **Limit check + auto-cleanup** — if at `maxWorktrees` (default 25), try `makeRoom()` first -7. **Create new** — call `provider.create(isolationRequest)` then `store.create()` - -If `store.create()` fails after `provider.create()` succeeds, the orphaned worktree is cleaned up best-effort before re-throwing. - -## Error Handling Pattern - -```typescript -import { classifyIsolationError, isKnownIsolationError } from '@archon/isolation'; - -try { - await provider.create(request); -} catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - if (!isKnownIsolationError(err)) { - throw err; // Unknown = programming bug, propagate as crash - } - const userMessage = classifyIsolationError(err); // Maps to friendly message - // ...send userMessage to platform, return blocked resolution -} -``` - -Known error patterns: `permission denied`, `eacces`, `timeout`, `no space left`, `enospc`, `not a git repository`, `branch not found`. - -`IsolationBlockedError` signals ALL message handling should stop — the user has already been notified. - -## Git Safety Rules - -- **NEVER run `git clean -fd`** — permanently deletes untracked files. Use `git checkout .` instead. -- **Always use `execFileAsync`** (from `@archon/git/exec`), never `exec` or `execSync` -- `hasUncommittedChanges()` returns `true` on unexpected errors (conservative — prevents data loss) -- Worktree paths follow project-scoped layout: `~/.archon/workspaces/{owner}/{repo}/worktrees/{branch}` - -## Architecture - -- `@archon/git` — zero `@archon/*` dependencies; only branded types and `execFileAsync` wrapper -- `@archon/isolation` — depends only on `@archon/git` + `@archon/paths` -- `IIsolationStore` interface injected into `IsolationResolver` — never call DB directly from git package -- `IIsolationProvider` interface — `WorktreeProvider` is the only implementation -- Stale env cleanup is best-effort: `markDestroyedBestEffort()` logs errors but never throws - -## Anti-patterns - -- Never call `git` via `exec()` or shell string — always `execFileAsync('git', [...args])` -- Never treat `IsolationBlockedError` as recoverable — it means user was notified, stop processing -- Never use a plain `string` where `RepoPath` / `BranchName` / `WorktreePath` is expected -- Never skip the `isKnownIsolationError()` check — unknown errors must propagate as crashes diff --git a/.claude/rules/orchestrator.md b/.claude/rules/orchestrator.md deleted file mode 100644 index acc3d64fa0..0000000000 --- a/.claude/rules/orchestrator.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -paths: - - "packages/core/src/orchestrator/**/*.ts" - - "packages/core/src/handlers/**/*.ts" - - "packages/core/src/state/**/*.ts" ---- - -# Orchestrator Conventions - -## Message Flow — Routing Agent Architecture - -``` -Platform message - → ConversationLockManager.acquireLock() - → handleMessage() (orchestrator-agent.ts:383) - → inheritThreadContext() — copy parent's codebase/cwd if child thread - → Deterministic gate: 10 commands (help, status, reset, workflow, register-project, update-project, remove-project, commands, init, worktree) - → Everything else → AI routing call: - → listCodebases() + discoverAllWorkflows() - → buildFullPrompt() → buildOrchestratorPrompt() or buildProjectScopedPrompt() - → AI responds with natural language ± /invoke-workflow or /register-project - → parseOrchestratorCommands() extracts structured commands from AI response - → If /invoke-workflow found → dispatchOrchestratorWorkflow() - → If /register-project found → handleRegisterProject() - → Otherwise → send AI text to user -``` - -Lock manager returns `{ status: 'started' | 'queued-conversation' | 'queued-capacity' }`. Always use the return value to decide whether to emit a "queued" notice — never call `isActive()` separately (TOCTOU race). - -## Deterministic Commands (command-handler.ts) - -Only **10 commands** are handled deterministically: - -| Command | Behavior | -|---------|----------| -| `/help` | Show available commands | -| `/status` | Show conversation/session state | -| `/reset` | Deactivate current session | -| `/workflow` | Subcommands: `list`, `run`, `status`, `cancel`, `reload` | -| `/register-project` | Handled inline — creates codebase DB record | -| `/update-project` | Handled inline — updates codebase path | -| `/remove-project` | Handled inline — deletes codebase DB record | -| `/commands` | List registered codebase commands | -| `/init` | Scaffold `.archon/` in current repo | -| `/worktree` | Worktree subcommands | - -**All other slash commands fall through to the AI router.** Unrecognized commands return an "Unknown command" error. - -## Routing AI — Prompt Building (prompt-builder.ts) - -The choice between prompts depends on whether the conversation has an attached project: - -- **No project** → `buildOrchestratorPrompt()` (prompt-builder.ts:116) — lists all projects equally, asks user to clarify if ambiguous -- **Has project** → `buildProjectScopedPrompt()` (prompt-builder.ts:153) — active project shown first, ambiguous requests default to it - -Both prompts include: registered projects, discovered workflows, and the `/invoke-workflow` + `/register-project` format specification. - -### `/invoke-workflow` Protocol - -The AI emits: `/invoke-workflow --project --prompt "user's intent"` - -`parseOrchestratorCommands()` (orchestrator-agent.ts:90) parses this with: -- Workflow name validated against discovered workflows via `findWorkflow()` -- Project name validated via `findCodebaseByName()` — case-insensitive, supports partial path segment match (e.g., `"repo"` matches `"owner/repo"`) -- `--project` must appear before `--prompt` - -### `filterToolIndicators()` (orchestrator-agent.ts:163) - -Batch mode only. Strips paragraphs starting with emoji tool indicators (🔧💭📝✏️🗑️📂🔍) from accumulated AI response before sending to user. - -## Session Transitions - -Sessions are **immutable** — never mutated, only deactivated and replaced. The audit trail is via `parent_session_id` + `transition_reason`. - -**Only `plan-to-execute` immediately creates a new session.** All other triggers only deactivate; the new session is created on the next AI message. - -```typescript -import { getTriggerForCommand, shouldCreateNewSession } from '../state/session-transitions'; - -const trigger = getTriggerForCommand('reset'); // 'reset-requested' -if (shouldCreateNewSession(trigger)) { - // plan-to-execute only -} -``` - -`TransitionTrigger` values: `'first-message'`, `'plan-to-execute'`, `'isolation-changed'`, `'reset-requested'`, `'worktree-removed'`, `'conversation-closed'`. - -## Isolation Resolution - -`validateAndResolveIsolation()` (orchestrator.ts:108) delegates to `IsolationResolver` and handles: -- Sending contextual messages to the platform (e.g., "Reusing worktree from issue #42") -- Updating the DB (`conversation.isolation_env_id`, `conversation.cwd`) -- Retrying once when a stale reference is found (`stale_cleaned`) -- Throwing `IsolationBlockedError` after platform notification when blocked - -When isolation is blocked, **stop all further processing** — `IsolationBlockedError` means the user was already notified. - -## Background Workflow Dispatch (Web only) - -`dispatchBackgroundWorkflow()` (orchestrator.ts:256) creates a hidden worker conversation (`web-worker-{timestamp}-{random}`), sets up event bridging from worker SSE → parent SSE, pre-creates the workflow run row (prevents 404 on immediate UI navigation), and fires-and-forgets `executeWorkflow()`. On completion, surfaces `result.summary` to the parent conversation. - -## Lazy Logger Pattern - -All files in this area use the deferred logger pattern — NEVER initialize at module scope: - -```typescript -let cachedLog: ReturnType | undefined; -function getLog(): ReturnType { - if (!cachedLog) cachedLog = createLogger('orchestrator'); - return cachedLog; -} -``` - -## Anti-patterns - -- Never call `isActive()` and then `acquireLock()` — race condition, use the lock return value -- Never access `conversation.isolation_env_id` directly without going through the resolver -- Never skip `IsolationBlockedError` — it must propagate to stop all further message handling -- Never add platform-specific logic to the orchestrator; it uses `IPlatformAdapter` interface only -- Never transition sessions by mutating them; always deactivate and create a new linked session -- Never assume a slash command is deterministic — only the 10 listed above bypass the AI router diff --git a/.claude/rules/server-api.md b/.claude/rules/server-api.md deleted file mode 100644 index 912e7db877..0000000000 --- a/.claude/rules/server-api.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -paths: - - "packages/server/**/*.ts" ---- - -# Server API Conventions - -## Hono Framework - -```typescript -import { Hono } from 'hono'; -import { streamSSE } from 'hono/streaming'; -import { cors } from 'hono/cors'; - -// CORS: allow-all for single-developer tool (override with WEB_UI_ORIGIN) -app.use('/api/*', cors({ origin: process.env.WEB_UI_ORIGIN || '*' })); - -// Error response helper pattern -function apiError(c: Context, status: 400 | 404 | 500, message: string): Response { - return c.json({ error: message }, status); -} -``` - -## SSE Streaming - -Always check `stream.closed` before writing. Use `stream.onAbort()` for cleanup. Hono's `streamSSE` callback receives an SSE writer: - -```typescript -app.get('/api/stream/:id', (c) => { - return streamSSE(c, async (stream) => { - stream.onAbort(() => { - transport.removeStream(conversationId, writer); - }); - // Write events: - if (!stream.closed) { - await stream.writeSSE({ data: JSON.stringify(event) }); - } - }); -}); -``` - -`SSETransport` in `src/adapters/web/transport.ts` manages the stream registry. `removeStream()` accepts an `expectedStream` reference to prevent race conditions (StrictMode double-mount). - -## Webhook Signature Verification - -```typescript -// ALWAYS use c.req.text() for raw webhook body — JSON.parse separately -const payload = await c.req.text(); -const signature = c.req.header('X-Hub-Signature-256') ?? ''; - -// timingSafeEqual prevents timing attacks -const hmac = createHmac('sha256', webhookSecret); -const digest = 'sha256=' + hmac.update(payload).digest('hex'); -const isValid = timingSafeEqual(Buffer.from(digest), Buffer.from(signature)); -``` - -Return 200 immediately for webhook events; process async. Never log the full signature. - -## Auto Port Allocation (Worktrees) - -`getPort()` from `@archon/core` returns: -- Main repo: `PORT` env var or `3090` -- Worktrees: hash-based port in range 3190–4089 (deterministic per worktree path) - -Same worktree always gets same port. Override with `PORT=4000` env var. - -## Static SPA Fallback - -```typescript -// Serve web dist; fall back to index.html for client-side routing -app.use('/*', serveStatic({ root: path.join(import.meta.dir, '../../web/dist') })); -app.get('*', (c) => c.html(/* index.html */)); -``` - -Use `import.meta.dir` (absolute) NOT relative paths — `bun --filter @archon/server start` changes CWD to `packages/server/`. - -## Graceful Shutdown - -```typescript -process.on('SIGTERM', () => { - stopCleanupScheduler(); - void pool.close(); - process.exit(0); -}); -``` - -## Key API Routes - -| Method | Path | Purpose | -|--------|------|---------| -| GET | `/api/conversations` | List conversations | -| POST | `/api/conversations` | Create conversation | -| POST | `/api/conversations/:id/message` | Send message | -| GET | `/api/stream/:id` | SSE stream | -| GET | `/api/workflows` | List workflows | -| POST | `/api/workflows/validate` | Validate YAML (in-memory) | -| GET | `/api/workflows/:name` | Get single workflow | -| PUT | `/api/workflows/:name` | Save workflow | -| DELETE | `/api/workflows/:name` | Delete workflow | -| GET | `/api/commands` | List commands | -| POST | `/webhooks/github` | GitHub webhook | - -## Anti-patterns - -- Never use `c.req.json()` for webhooks — signature must be verified against raw body -- Never expose API keys in JSON error responses -- Never serve static files with relative paths (use `import.meta.dir`) -- Never skip the `stream.closed` check before writing SSE -- Never call platform adapters directly from route handlers — use `handleMessage()` + lock manager diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md deleted file mode 100644 index 030f697539..0000000000 --- a/.claude/rules/testing.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -paths: - - "**/*.test.ts" - - "**/*.spec.ts" ---- - -# Testing Conventions - -## CRITICAL: mock.module() Pollution Rules - -`mock.module()` permanently replaces modules in the **process-wide module cache**. `mock.restore()` does NOT undo it ([oven-sh/bun#7823](https://github.com/oven-sh/bun/issues/7823)). - -**Rules:** -1. **Never add `afterAll(() => mock.restore())` for `mock.module()` calls** — it has no effect -2. **Never have two test files `mock.module()` the same path with different implementations in the same `bun test` invocation** -3. **Use `spyOn()` for internal modules** — `spy.mockRestore()` DOES work for spies - -```typescript -// CORRECT: spy (restorable) -import * as git from '@archon/git'; -const spy = spyOn(git, 'checkout'); -spy.mockImplementation(async () => ({ ok: true, value: undefined })); -// afterEach: -spy.mockRestore(); - -// CORRECT: mock.module() for external deps (not restorable — isolate in separate test file) -mock.module('@slack/bolt', () => ({ App: mock(() => mockApp), LogLevel: { INFO: 'info' } })); -``` - -## Test Batching Per Package - -Each package splits tests into separate `bun test` invocations to prevent pollution: - -| Package | Batches | -|---------|---------| -| `@archon/core` | 7 batches (clients, handlers, db+utils, path-validation, cleanup-service, title-generator, workflows, orchestrator) | -| `@archon/workflows` | 5 batches | -| `@archon/adapters` | 3 batches (chat+community+forge-auth, github-adapter, github-context) | -| `@archon/isolation` | 3 batches | - -**Never run `bun test` from the repo root** — causes ~135 mock pollution failures. Always use: - -```bash -bun run test # Correct: per-package isolation via bun --filter '*' test -bun run test --watch # Watch mode (single package) -``` - -## Mock Pattern for Lazy Loggers - -All adapter/db/orchestrator files use lazy logger pattern. Mock before import: - -```typescript -// MUST come before import of the module under test -const mockLogger = { - fatal: mock(() => undefined), error: mock(() => undefined), - warn: mock(() => undefined), info: mock(() => undefined), - debug: mock(() => undefined), trace: mock(() => undefined), -}; -mock.module('@archon/paths', () => ({ createLogger: mock(() => mockLogger) })); - -import { SlackAdapter } from './adapter'; // Import AFTER mock -``` - -## Database Test Mocking - -```typescript -import { createQueryResult, mockPostgresDialect } from '../test/mocks/database'; - -const mockQuery = mock(() => Promise.resolve(createQueryResult([]))); -mock.module('./connection', () => ({ - pool: { query: mockQuery }, - getDialect: () => mockPostgresDialect, -})); - -// In tests: -mockQuery.mockResolvedValueOnce(createQueryResult([existingRow])); -mockQuery.mockClear(); // in beforeEach -``` - -## Test Structure - -```typescript -import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test'; - -describe('ComponentName', () => { - beforeEach(() => { - mockFn.mockClear(); // Reset call counts - }); - - test('does thing when condition', async () => { - mockQuery.mockResolvedValueOnce(createQueryResult([fixture])); - const result = await functionUnderTest(input); - expect(result).toEqual(expected); - expect(mockQuery).toHaveBeenCalledTimes(1); - }); -}); -``` - -## Anti-patterns - -- Never `import` a module before all `mock.module()` calls for its dependencies -- Never use `afterAll(() => mock.restore())` for `mock.module()` — it silently does nothing -- Never test with real database or filesystem in unit tests — always mock -- Never run `bun test` from the repo root -- Never add a new test file with conflicting `mock.module()` to an existing batch — create a new batch in the package's `package.json` test script diff --git a/.claude/rules/web-frontend.md b/.claude/rules/web-frontend.md deleted file mode 100644 index 7811997fde..0000000000 --- a/.claude/rules/web-frontend.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -paths: - - "packages/web/**/*.tsx" - - "packages/web/**/*.ts" - - "packages/web/**/*.css" ---- - -# Web Frontend Conventions - -## Tech Stack - -- React 19 + Vite 6 + TypeScript -- Tailwind CSS v4 (CSS-first config) -- shadcn/ui components -- TanStack Query v5 for REST data -- React Router v7 (`react-router`, NOT `react-router-dom`) -- Manual `EventSource` for SSE streaming (no library) -- **Dark theme only** — no light mode toggle - -## Tailwind v4 Critical Differences - -```css -/* CORRECT: CSS-first import */ -@import 'tailwindcss'; -@import 'tw-animate-css'; /* NOT tailwindcss-animate */ - -/* CORRECT: theme variables in @theme inline block */ -@theme inline { - --color-surface: var(--surface); - --color-accent-bright: var(--accent-bright); -} - -/* WRONG: never use @tailwind base/components/utilities */ -``` - -Plugin in `vite.config.ts`: `import tailwindcss from '@tailwindcss/vite'` — uses Vite plugin, **not PostCSS**. `components.json` has blank `tailwind.config` for v4. - -## Color Palette (oklch) - -All custom colors are OKLCH. Key tokens (defined in `:root` in `index.css`): -- `--surface` (0.18): main surface -- `--surface-elevated` (0.22): cards, popovers -- `--background` (0.14): page background -- `--primary` / `--ring`: blue accent at oklch(0.65 0.18 250) -- `--text-primary` (0.93), `--text-secondary` (0.65), `--text-tertiary` (0.45) -- `--success` (green 155), `--warning` (yellow 75), `--error` (red 25) - -Use CSS variables via Tailwind utilities: `bg-surface`, `text-text-primary`, `border-border`, `text-accent-bright`, etc. - -## SSE Streaming Pattern - -`useSSE()` in `src/hooks/useSSE.ts` is the single SSE consumer. It: -- Opens `EventSource` to `/api/stream/{conversationId}` -- Batches text events (50ms flush timer) to reduce re-renders -- Flushes immediately before `tool_call`, `tool_result`, `workflow_dispatch` events -- Marks disconnected only on `CLOSED` state (not `CONNECTING` — avoids flicker) -- `handlersRef` pattern ensures stable EventSource with fresh handlers - -Event types: `text`, `tool_call`, `tool_result`, `error`, `conversation_lock`, `session_info`, `workflow_step`, `workflow_status`, `parallel_agent`, `workflow_artifact`, `dag_node`, `workflow_dispatch`, `workflow_output_preview`, `warning`, `retract`, `heartbeat`. - -## Routing - -```tsx -// CORRECT -import { BrowserRouter, Routes, Route } from 'react-router'; -// WRONG -import { BrowserRouter } from 'react-router-dom'; -``` - -Routes: `/` (Dashboard), `/chat`, `/chat/*`, `/workflows`, `/workflows/builder`, `/workflows/runs/:runId`, `/settings`. - -## API Client Pattern - -```typescript -// src/lib/api.ts exports SSE_BASE_URL and REST functions -import { SSE_BASE_URL } from '@/lib/api'; -// In dev: Vite proxies /api/* to localhost:{VITE_API_PORT} -// API port injected at build time: import.meta.env.VITE_API_PORT -``` - -TanStack Query `staleTime: 10_000`, `refetchOnWindowFocus: true`. - -## Anti-patterns - -- Never add a light mode — dark-only is intentional -- Never use `react-router-dom` — use `react-router` (v7) -- Never configure Tailwind in `tailwind.config.js/ts` — v4 is CSS-first -- Never use `tailwindcss-animate` — use `tw-animate-css` -- Never open a second `EventSource` per conversation — `useSSE()` handles it -- Never pass inline style objects for theme colors — use Tailwind classes with CSS variables diff --git a/.claude/rules/workflows.md b/.claude/rules/workflows.md deleted file mode 100644 index 99cf6f8913..0000000000 --- a/.claude/rules/workflows.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -paths: - - "packages/workflows/**/*.ts" - - ".archon/workflows/**/*.yaml" - - ".archon/commands/**/*.md" ---- - -# Workflows Conventions - -## DAG Workflow Format - -All workflows use the DAG (Directed Acyclic Graph) format with `nodes:`. Loop nodes are supported as a node type within DAGs. - -```yaml -nodes: - - id: classify - prompt: "Is this a bug or feature? Answer JSON: {type: 'BUG'|'FEATURE'}" - output_format: {type: object, properties: {type: {type: string}}} - - id: implement - command: execute - depends_on: [classify] - when: "$classify.output.type == 'FEATURE'" - - id: run_lint - bash: "bun run lint" - depends_on: [implement] - - id: iterate - loop: - until: "COMPLETE" - max_iterations: 10 - prompt: "Iterate until the tests pass. Signal COMPLETE when done." - depends_on: [run_lint] -``` - -## Variable Substitution - -| Variable | Resolved to | -|----------|-------------| -| `$1`, `$2`, `$3` | Positional arguments from user message | -| `$ARGUMENTS` | All user arguments as single string | -| `$ARTIFACTS_DIR` | Pre-created external artifacts directory | -| `$WORKFLOW_ID` | Current workflow run ID | -| `$BASE_BRANCH` | Base branch from config or auto-detected | -| `$DOCS_DIR` | Documentation directory path (default: `docs/`) | -| `$nodeId.output` | Captured stdout/AI output from completed DAG node | - -## WorkflowDeps — Dependency Injection - -`@archon/workflows` has ZERO `@archon/core` dependency. Everything is injected: - -```typescript -interface WorkflowDeps { - store: IWorkflowStore; // DB abstraction - getAssistantClient: AssistantClientFactory; // Returns claude or codex client - loadConfig: (cwd: string) => Promise; -} - -// Core creates the adapter: -import { createWorkflowDeps } from '@archon/core/workflows/store-adapter'; -const deps = createWorkflowDeps(); -await executeWorkflow(deps, platform, conversationId, cwd, workflow, ...); -``` - -## DAG Node Types - -- `command:` — named file from `.archon/commands/`, AI-executed -- `prompt:` — inline prompt string, AI-executed -- `bash:` — shell script, no AI; stdout captured as `$nodeId.output`; default timeout 120000ms -- `script:` — inline code or named file from `.archon/scripts/`, runs via `runtime: bun` (`.ts`/`.js`) or `runtime: uv` (`.py`), no AI; stdout captured as `$nodeId.output`; supports `deps:` for dependency installation and `timeout:` (ms); runtime availability checked at load time with a warning if binary is missing - -DAG node options: `depends_on`, `when` (condition expression), `trigger_rule` (`all_success` | `one_success` | `none_failed_min_one_success` | `all_done`), `output_format` (JSON Schema, Claude only), `allowed_tools` / `denied_tools` (Claude only), `idle_timeout` (ms), `context: 'fresh'`, per-node `provider` and `model`, `deps` (script nodes only — dependency list), `runtime` (script nodes only — `'bun'` or `'uv'`). - -## Event Emitter for Observability - -```typescript -import { getWorkflowEventEmitter } from '@archon/workflows'; - -const emitter = getWorkflowEventEmitter(); -emitter.registerRun(runId, conversationId); - -// Subscribe (returns unsubscribe fn) -const unsubscribe = emitter.subscribeForConversation(conversationId, (event) => { - // event.type: 'step_started' | 'step_completed' | 'node_started' | ... -}); -``` - -Listener errors never propagate to the executor — fire-and-forget with internal catch. - -## Architecture - -- Model validation at load time — invalid provider/model combinations fail `parseWorkflow()` with clear error -- Resilient discovery — one broken YAML doesn't abort `discoverWorkflows()`; errors returned in `WorkflowLoadResult.errors` -- Bundled defaults embedded in binary builds; loaded from filesystem in source builds -- Repo workflows override bundled defaults by name -- Router fallback: if no `/invoke-workflow` produced → falls back to `archon-assist`; raw AI response only when `archon-assist` unavailable - -## Anti-patterns - -- Never import `@archon/core` from `@archon/workflows` (circular dependency) -- Never add `clearContext: true` to every step — context continuity is valuable; use sparingly -- Never put `output_format` on Codex nodes — it logs a warning and is ignored -- Never set `allowed_tools: undefined` expecting "no tools" — use `allowed_tools: []` for that diff --git a/.prettierignore b/.prettierignore index 5f7484c1a6..449d3ded7f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -41,3 +41,7 @@ CHANGELOG.md CONTRIBUTING.md *.yml *.yaml + +# Pre-existing intentional formatting exceptions (do not reformat) +packages/web/index.html +packages/web/src/lib/useVisualViewport.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 29fb4e1166..a2201632b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -179,7 +179,7 @@ Chat-first navigation redesign, DAG graph viewer, per-node MCP and skills, and e - Idle timeout not detecting stuck tool calls during execution (#649) - `commitAllChanges` failing on empty commits (#745) - Explicit base branch config now required for worktree creation (#686) -- Subprocess-level retry added to CodexClient (#641) +- Subprocess-level retry added to CodexProvider (#641) - Validate `cwd` query param against registered codebases (#630) - Server-internal paths redacted from `/api/config` response (#632) - SQLite conversations index missing `WHERE deleted_at IS NULL` (#629) @@ -231,7 +231,7 @@ DAG hardening, security fixes, validate-pr workflow, and worktree lifecycle mana - **`--json` flag for `workflow list`** — machine-readable workflow output (#594) - **`archon-validate-pr` workflow** with per-node idle timeout support (#635) - **Typed SessionMetadata** with Zod validation for safer metadata handling (#600) -- **`persistSession: false`** in ClaudeClient to avoid disk pollution from session transcripts (#626) +- **`persistSession: false`** in ClaudeProvider to avoid disk pollution from session transcripts (#626) - **DAG workflow for GitHub issue resolution** with structured node pipeline ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 0e902537dd..a2b9d8d973 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ These are implementation constraints, not slogans. Apply them by default. **SRP + ISP — Single Responsibility + Interface Segregation** - Keep each module and package focused on one concern -- Extend behavior by implementing existing narrow interfaces (`IPlatformAdapter`, `IAssistantClient`, `IDatabase`, `IWorkflowStore`) whenever possible +- Extend behavior by implementing existing narrow interfaces (`IPlatformAdapter`, `IAgentProvider`, `IDatabase`, `IWorkflowStore`) whenever possible - Avoid fat interfaces and "god modules" that mix policy, transport, and storage - Do not add unrelated methods to an existing interface — define a new one @@ -122,7 +122,7 @@ bun test --watch # Watch mode (single package) bun test packages/core/src/handlers/command-handler.test.ts # Single file ``` -**Test isolation (mock.module pollution):** Bun's `mock.module()` permanently replaces modules in the process-wide cache — `mock.restore()` does NOT undo it ([oven-sh/bun#7823](https://github.com/oven-sh/bun/issues/7823)). To prevent cross-file pollution, packages that have conflicting `mock.module()` calls split their tests into separate `bun test` invocations: `@archon/core` (7 batches), `@archon/workflows` (5), `@archon/adapters` (4), `@archon/isolation` (3). See each package's `package.json` for the exact splits. +**Test isolation (mock.module pollution):** Bun's `mock.module()` permanently replaces modules in the process-wide cache — `mock.restore()` does NOT undo it ([oven-sh/bun#7823](https://github.com/oven-sh/bun/issues/7823)). To prevent cross-file pollution, packages that have conflicting `mock.module()` calls split their tests into separate `bun test` invocations: `@archon/core` (7 batches), `@archon/workflows` (5), `@archon/adapters` (3), `@archon/isolation` (3). See each package's `package.json` for the exact splits. **Do NOT run `bun test` from the repo root** — it discovers all test files across all packages and runs them in one process, causing ~135 mock pollution failures. Always use `bun run test` (which uses `bun --filter '*' test` for per-package isolation). @@ -198,10 +198,6 @@ bun run cli workflow run implement --branch feature-auth "Add auth" # Opt out of isolation (run in live checkout) bun run cli workflow run quick-fix --no-worktree "Fix typo" -# Grant env-leak-gate consent during auto-registration (for repos whose .env -# contains sensitive keys). Audit-logged with actor: 'user-cli'. -bun run cli workflow run plan --cwd /path/to/leaky/repo --allow-env-keys "..." - # Show running workflows bun run cli workflow status @@ -266,9 +262,16 @@ packages/ │ ├── adapters/ # CLI adapter (stdout output) │ ├── commands/ # CLI command implementations │ └── cli.ts # CLI entry point +├── providers/ # @archon/providers - AI agent providers (SDK deps live here) +│ └── src/ +│ ├── types.ts # Contract layer (IAgentProvider, SendQueryOptions, MessageChunk — ZERO SDK deps) +│ ├── factory.ts # getAgentProvider() switch (built-in: claude, codex) +│ ├── errors.ts # UnknownProviderError +│ ├── claude/ # ClaudeProvider + parseClaudeConfig + MCP/hooks/skills translation +│ ├── codex/ # CodexProvider + parseCodexConfig + binary-resolver +│ └── index.ts # Package exports ├── core/ # @archon/core - Shared business logic │ └── src/ -│ ├── clients/ # AI SDK clients (Claude, Codex) │ ├── config/ # YAML config loading │ ├── db/ # Database connection, queries │ ├── handlers/ # Command handler (slash commands) @@ -289,7 +292,7 @@ packages/ │ ├── executor.ts # Workflow execution orchestrator (executeWorkflow) │ ├── dag-executor.ts # DAG-specific execution logic │ ├── store.ts # IWorkflowStore interface (database abstraction) -│ ├── deps.ts # WorkflowDeps injection types (IWorkflowPlatform, IWorkflowAssistantClient) +│ ├── deps.ts # WorkflowDeps injection types (IWorkflowPlatform, imports from @archon/providers/types) │ ├── event-emitter.ts # Workflow observability events │ ├── logger.ts # JSONL file logger │ ├── validator.ts # Resource validation (command files, MCP configs, skill dirs) @@ -401,10 +404,11 @@ import type { DagNode, WorkflowDefinition } from '@/lib/api'; **Package Split:** - **@archon/paths**: Path resolution utilities, Pino logger factory, web dist cache path (`getWebDistDir`), CWD env stripper (`stripCwdEnv`, `strip-cwd-env-boot`) (no @archon/* deps; `pino` and `dotenv` are allowed external deps) - **@archon/git**: Git operations - worktrees, branches, repos, exec wrappers (depends only on @archon/paths) +- **@archon/providers**: AI agent providers (Claude, Codex) — owns SDK deps, `IAgentProvider` interface, `sendQuery()` contract, and provider-specific option translation. `@archon/providers/types` is the contract subpath (zero SDK deps, zero runtime side effects) that `@archon/workflows` imports from. Providers receive raw `nodeConfig` + `assistantConfig` and translate to SDK-specific options internally. - **@archon/isolation**: Worktree isolation types, providers, resolver, error classifiers (depends only on @archon/git + @archon/paths) -- **@archon/workflows**: Workflow engine - loader, router, executor, DAG, logger, bundled defaults (depends only on @archon/git + @archon/paths + @hono/zod-openapi + zod; DB/AI/config injected via `WorkflowDeps`) +- **@archon/workflows**: Workflow engine - loader, router, executor, DAG, logger, bundled defaults (depends only on @archon/git + @archon/paths + @archon/providers/types + @hono/zod-openapi + zod; DB/AI/config injected via `WorkflowDeps`) - **@archon/cli**: Command-line interface for running workflows and starting the web UI server (depends on @archon/server + @archon/adapters for the serve command) -- **@archon/core**: Business logic, database, orchestration, AI clients (provides `createWorkflowStore()` adapter bridging core DB → `IWorkflowStore`) +- **@archon/core**: Business logic, database, orchestration (depends on @archon/providers for AI; provides `createWorkflowStore()` adapter bridging core DB → `IWorkflowStore`) - **@archon/adapters**: Platform adapters for Slack, Telegram, GitHub, Discord (depends on @archon/core) - **@archon/server**: OpenAPIHono HTTP server (Zod + OpenAPI spec generation via `@hono/zod-openapi`), Web adapter (SSE), API routes, Web UI static serving (depends on @archon/adapters) - **@archon/web**: React frontend (Vite + Tailwind v4 + shadcn/ui + Zustand), SSE streaming to server. `WorkflowRunStatus`, `WorkflowDefinition`, and `DagNode` are all derived from `src/lib/api.generated.d.ts` (generated from the OpenAPI spec via `bun generate:types`; never import from `@archon/workflows`) @@ -429,7 +433,8 @@ import type { DagNode, WorkflowDefinition } from '@/lib/api'; **2. Command Handler** (`packages/core/src/handlers/`) - Process slash commands (deterministic, no AI) -- Commands: `/command-set`, `/load-commands`, `/clone`, `/getcwd`, `/setcwd`, `/repos`, `/repo`, `/repo-remove`, `/worktree`, `/workflow`, `/status`, `/commands`, `/help`, `/reset`, `/reset-context`, `/init` +- The orchestrator treats only these top-level commands as deterministic: `/help`, `/status`, `/reset`, `/workflow`, `/register-project`, `/update-project`, `/remove-project`, `/commands`, `/init`, `/worktree` +- `/workflow` handles subcommands like `list`, `run`, `status`, `cancel`, `resume`, `abandon`, `approve`, `reject` - Update database, perform operations, return responses **3. Orchestrator** (`packages/core/src/orchestrator/`) @@ -439,10 +444,10 @@ import type { DagNode, WorkflowDefinition } from '@/lib/api'; - Session management: Create new or resume existing - Stream AI responses to platform -**4. AI Assistant Clients** (`packages/core/src/clients/`) -- Implement `IAssistantClient` interface -- **ClaudeClient**: `@anthropic-ai/claude-agent-sdk` -- **CodexClient**: `@openai/codex-sdk` +**4. AI Agent Providers** (`packages/providers/src/`) +- Implement `IAgentProvider` interface +- **ClaudeProvider**: `@anthropic-ai/claude-agent-sdk` +- **CodexProvider**: `@openai/codex-sdk` - Streaming: `for await (const event of events) { await platform.send(event) }` ### Configuration @@ -530,7 +535,7 @@ curl http://localhost:3637/api/conversations//messages ``` ~/.archon/ ├── workspaces/owner/repo/ # Project-centric layout -│ ├── source/ # Clone (from /clone) or symlink → local path +│ ├── source/ # Cloned repo or symlink → local path │ ├── worktrees/ # Git worktrees for this project │ ├── artifacts/ # Workflow artifacts (NEVER in git) │ │ ├── runs/{id}/ # Per-run artifacts ($ARTIFACTS_DIR) @@ -561,7 +566,7 @@ curl http://localhost:3637/api/conversations//messages **Quick reference:** - **Platform Adapters**: Implement `IPlatformAdapter`, handle auth, polling/webhooks -- **AI Clients**: Implement `IAssistantClient`, session management, streaming +- **AI Providers**: Implement `IAgentProvider`, session management, streaming - **Slash Commands**: Add to command-handler.ts, update database, no AI - **Database Operations**: Use `IDatabase` interface (supports PostgreSQL and SQLite via adapters) @@ -675,8 +680,8 @@ async function createSession(conversationId: string, codebaseId: string) { 1. **Codebase Commands** (per-repo): - Stored in `.archon/commands/` (plain text/markdown) - - Auto-detected via `/clone` or `/load-commands ` - - Loaded by `/clone` or `/load-commands`, invoked by AI via orchestrator routing + - Discovered from the repository `.archon/commands/` directory + - Surfaced via `GET /api/commands` for the workflow builder and invoked by workflow `command:` nodes 2. **Workflows** (YAML-based): - Stored in `.archon/workflows/` (searched recursively) @@ -759,9 +764,11 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er **Codebases:** - `GET /api/codebases` / `GET /api/codebases/:id` - List / fetch codebases -- `POST /api/codebases` - Register a codebase (clone or local path); body accepts `allowEnvKeys` for the env-leak gate -- `PATCH /api/codebases/:id` - Flip the `allow_env_keys` consent bit; body: `{ allowEnvKeys: boolean }`. Audit-logged at `warn` level on every grant/revoke (`env_leak_consent_granted` / `env_leak_consent_revoked`) with `codebaseId`, `path`, `files`, `keys`, `scanStatus`, `actor` +- `POST /api/codebases` - Register a codebase (clone or local path) - `DELETE /api/codebases/:id` - Delete a codebase and clean up resources +- `GET /api/codebases/:id/env` - List env var keys for a codebase (never returns values) +- `PUT /api/codebases/:id/env` / `DELETE /api/codebases/:id/env/:key` - Upsert / delete a single codebase env var +- `GET /api/codebases/:id/environments` - List tracked isolation environments for a codebase **Artifact Files:** - `GET /api/artifacts/:runId/*` - Serve a workflow artifact file by run ID and relative path; returns `text/markdown` for `.md` files, `text/plain` otherwise; 400 on path traversal (`..`), 404 if run or file not found @@ -770,6 +777,7 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er - `GET /api/commands` - List available command names (bundled + project-defined); optional `?cwd=`; returns `{ commands: [{ name, source: 'bundled' | 'project' }] }` **System:** +- `GET /api/health` - Health check with adapter/system status - `GET /api/update-check` - Check for available updates; returns `{ updateAvailable, currentVersion, latestVersion, releaseUrl }`; skips GitHub API call for non-binary builds **OpenAPI Spec:** diff --git a/Dockerfile b/Dockerfile index da4783e019..139b3efaf7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY packages/docs-web/package.json ./packages/docs-web/ COPY packages/git/package.json ./packages/git/ COPY packages/isolation/package.json ./packages/isolation/ COPY packages/paths/package.json ./packages/paths/ +COPY packages/providers/package.json ./packages/providers/ COPY packages/server/package.json ./packages/server/ COPY packages/web/package.json ./packages/web/ COPY packages/workflows/package.json ./packages/workflows/ @@ -130,6 +131,7 @@ COPY packages/docs-web/package.json ./packages/docs-web/ COPY packages/git/package.json ./packages/git/ COPY packages/isolation/package.json ./packages/isolation/ COPY packages/paths/package.json ./packages/paths/ +COPY packages/providers/package.json ./packages/providers/ COPY packages/server/package.json ./packages/server/ COPY packages/web/package.json ./packages/web/ COPY packages/workflows/package.json ./packages/workflows/ @@ -144,6 +146,7 @@ COPY packages/core/ ./packages/core/ COPY packages/git/ ./packages/git/ COPY packages/isolation/ ./packages/isolation/ COPY packages/paths/ ./packages/paths/ +COPY packages/providers/ ./packages/providers/ COPY packages/server/ ./packages/server/ COPY packages/workflows/ ./packages/workflows/ diff --git a/RESPONSIVE_UI_PATCH.md b/RESPONSIVE_UI_PATCH.md new file mode 100644 index 0000000000..f9171afbde --- /dev/null +++ b/RESPONSIVE_UI_PATCH.md @@ -0,0 +1,805 @@ +# Patch : UI Mobile-Responsive — Archon + +> **Date du patch :** Avril 2026 +> **Auteur :** Roland Galzy +> **Version Archon ciblée :** branche `main` / tag à préciser lors de la prochaine mise à jour + +--- + +## Pourquoi ce patch existe + +Archon est déployé localement et exposé via un tunnel Cloudflare (`cloudflared`) pour permettre un accès depuis un smartphone ou une tablette. L'interface d'origine n'étant pas conçue pour les petits écrans (navigation en haut de page non adaptée, panneau latéral gauche du chat prenant toute la largeur), une série de modifications a été appliquée pour rendre l'UI pleinement utilisable sur mobile. + +**Changements principaux :** + +1. Création d'un **contexte React partagé** (`MobileNavContext`) pour synchroniser l'état du drawer mobile. +2. Ajout d'un **drawer de navigation latéral** dans `Layout.tsx` (visible uniquement sur mobile). +3. Transformation de la **TopNav** : bouton hamburger ☰ sur mobile, comportement inchangé sur desktop. +4. Masquage du **panneau gauche de ChatPage** sur mobile (conversations), la zone de chat prend toute la largeur. + +--- + +## Fichiers modifiés + +### 1. `packages/web/src/contexts/MobileNavContext.tsx` _(nouveau fichier)_ + +**Rôle :** Contexte React permettant à `Layout.tsx` et `TopNav.tsx` de partager l'état d'ouverture du drawer mobile (`open` / `setOpen`) sans props drilling. + +**Code complet :** + +```tsx +import { createContext, useContext } from 'react'; + +export interface MobileNavContextValue { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const MobileNavContext = createContext({ + open: false, + setOpen: () => {}, +}); + +export function useMobileNav(): MobileNavContextValue { + return useContext(MobileNavContext); +} +``` + +--- + +### 2. `packages/web/src/components/layout/Layout.tsx` _(modifié)_ + +**Changements apportés :** + +- Import de `useState`, des icônes Lucide (`MessageSquare`, `LayoutDashboard`, `Workflow`, `Settings`, `X`) et de `NavLink` depuis `react-router`. +- Import de `MobileNavContext` et `cn`. +- Ajout de l'état `open` / `setOpen` via `useState(false)`. +- Enveloppe le JSX dans ``. +- Ajout d'un **backdrop overlay** semi-transparent (cliquable pour fermer) : `fixed inset-0 z-40 bg-black/60 md:hidden`. +- Ajout d'un **drawer `