From 2ec25bfc666eedb8613202b2c5659cb3a8ad1de1 Mon Sep 17 00:00:00 2001 From: aWN4Y25pa2EK <19519604+aWN4Y25pa2EK@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:30:28 +0000 Subject: [PATCH 1/4] feat(docker): add Runtime Execution Layer with language-specific containers Add 6 Docker containers for HCA autonomous code execution: - Base: OpenCode 1.1.8, ast-grep, ripgrep, jq, yq, fd, python3 - Rust: rustc 1.92, cargo, clippy, nextest, sqlx-cli - TypeScript: Bun 1.3.5, Node 20, tsc, vitest, eslint, biome - Python: Python 3.11, uv 0.9, pytest, ruff, mypy - Generic: markdownlint, prettier, pandoc, aspell - Infrastructure: kubectl 1.35, helm 3.19, helmfile 1.2, kustomize Each container includes SKILL.md templates defining capabilities and allowed tools for OpenCode autonomous operation. Updates ARCHITECTURE.md with Runtime Execution Layer documentation including RuntimeCoordinatorHolon specification and routing logic. Relates to: ARDGBL-1073 Co-Authored-By: Claude Opus 4.5 --- impl/mvp/ARCHITECTURE.md | 325 +++++++++++++++++- impl/mvp/docker/opencode/README.md | 55 +-- impl/mvp/docker/runtimes/README.md | 122 +++++++ impl/mvp/docker/runtimes/base/Dockerfile | 133 +++++++ impl/mvp/docker/runtimes/base/entrypoint.sh | 39 +++ .../docker/runtimes/base/opencode-config.json | 19 + impl/mvp/docker/runtimes/generic/Dockerfile | 54 +++ impl/mvp/docker/runtimes/generic/SKILL.md | 82 +++++ .../docker/runtimes/infrastructure/Dockerfile | 66 ++++ .../docker/runtimes/infrastructure/SKILL.md | 105 ++++++ impl/mvp/docker/runtimes/python/Dockerfile | 56 +++ impl/mvp/docker/runtimes/python/SKILL.md | 100 ++++++ impl/mvp/docker/runtimes/rust/Dockerfile | 67 ++++ impl/mvp/docker/runtimes/rust/SKILL.md | 71 ++++ .../mvp/docker/runtimes/typescript/Dockerfile | 69 ++++ impl/mvp/docker/runtimes/typescript/SKILL.md | 105 ++++++ 16 files changed, 1444 insertions(+), 24 deletions(-) create mode 100644 impl/mvp/docker/runtimes/README.md create mode 100644 impl/mvp/docker/runtimes/base/Dockerfile create mode 100644 impl/mvp/docker/runtimes/base/entrypoint.sh create mode 100644 impl/mvp/docker/runtimes/base/opencode-config.json create mode 100644 impl/mvp/docker/runtimes/generic/Dockerfile create mode 100644 impl/mvp/docker/runtimes/generic/SKILL.md create mode 100644 impl/mvp/docker/runtimes/infrastructure/Dockerfile create mode 100644 impl/mvp/docker/runtimes/infrastructure/SKILL.md create mode 100644 impl/mvp/docker/runtimes/python/Dockerfile create mode 100644 impl/mvp/docker/runtimes/python/SKILL.md create mode 100644 impl/mvp/docker/runtimes/rust/Dockerfile create mode 100644 impl/mvp/docker/runtimes/rust/SKILL.md create mode 100644 impl/mvp/docker/runtimes/typescript/Dockerfile create mode 100644 impl/mvp/docker/runtimes/typescript/SKILL.md diff --git a/impl/mvp/ARCHITECTURE.md b/impl/mvp/ARCHITECTURE.md index a4fe188..b6f86ca 100644 --- a/impl/mvp/ARCHITECTURE.md +++ b/impl/mvp/ARCHITECTURE.md @@ -24,11 +24,13 @@ graph TB REVIEW["βœ… Reviewer Holon
holon:i2p:reviewer
TYPE: LEAF"] CURSOR["πŸ”„ Cursor Reviewer
holon:i2p:cursor-reviewer
TYPE: LEAF"] DEEP["🧠 Deep Reviewer
holon:i2p:deep-reviewer
TYPE: LEAF"] + RUNTIME["βš™οΈ Runtime Coordinator
holon:i2p:runtime-coordinator
TYPE: DOMAIN"] ROOT -->|"delegate (PLAN)"| ARCH ROOT -->|"delegate (REVIEW)"| REVIEW ROOT -->|"delegate (PR review)"| CURSOR ROOT -->|"delegate (deep review)"| DEEP + ROOT -->|"delegate (execution)"| RUNTIME ARCH -.->|"request review"| REVIEW end @@ -49,6 +51,14 @@ graph TB OPENCODE["πŸš€ OpenCode
Deep Reasoning"] end + subgraph "RUNTIME CONTAINERS" + RT_RUST["πŸ¦€ Rust
:4100"] + RT_TS["πŸ“˜ TypeScript
:4101"] + RT_PY["🐍 Python
:4102"] + RT_GEN["πŸ“„ Generic
:4103"] + RT_INFRA["☸️ Infrastructure
:4104"] + end + ACP --> ROOT A2A --> ROOT @@ -63,12 +73,18 @@ graph TB DEEP -.-> OC DEEP -.-> GIT ROOT -.-> LIN + RUNTIME -.-> RT_RUST + RUNTIME -.-> RT_TS + RUNTIME -.-> RT_PY + RUNTIME -.-> RT_GEN + RUNTIME -.-> RT_INFRA VEC --> QDRANT LLM --> OPENR LIN --> LINEAR GIT --> GITHUB OC --> OPENCODE + RT_RUST & RT_TS & RT_PY & RT_GEN & RT_INFRA --> OPENR ``` ## Holon Specifications @@ -158,6 +174,46 @@ Uses OpenCode for autonomous deep code analysis with extended thinking. | Can Negotiate | Yes | | Tools | OpenCode (external), artifact_retrieve, vector_search | +### Runtime Coordinator (`holon:i2p:runtime-coordinator`) + +Domain-level holon that routes execution tasks to appropriate runtime containers. + +```mermaid +graph TB + subgraph "Task Analysis" + PARSE["Parse Issue
Description"] + DETECT["Detect Project
Type"] + SELECT["Select Runtime
Container"] + end + + subgraph "Execution" + CONNECT["Connect to
Container"] + EXECUTE["Execute via
OpenCode API"] + COLLECT["Collect
Results"] + end + + PARSE --> DETECT --> SELECT --> CONNECT --> EXECUTE --> COLLECT +``` + +| Property | Value | +|----------|-------| +| Type | DOMAIN | +| Parent | holon:i2p:root | +| Can Delegate | Yes | +| Can Refuse | Yes | +| Tools | template_parser, container_registry, opencode_client | +| Manages | rust, typescript, python, generic, infrastructure containers | + +**Routing Algorithm:** + +1. Parse issue description using template parser +2. Check for project markers (Cargo.toml, package.json, etc.) +3. Fall back to file extension analysis +4. Select runtime container based on match +5. Connect to container's OpenCode API (port 4100-4104) +6. Execute task with appropriate SKILL.md +7. Return results upstream + ## Compound Loop ```mermaid @@ -324,6 +380,219 @@ Features: - **Hot Reload**: Watch config file for changes without restart - **Model Routing**: Route to instances supporting specific models +## Runtime Execution Layer + +The Runtime Execution Layer provides language-specific containers for autonomous code execution. Each container extends a common base image with OpenCode pre-configured for autonomous operation. + +### Architecture Overview + +```mermaid +graph TB + subgraph "HCA Core" + ROOT["Root Holon"] + COORD["Runtime Coordinator
holon:i2p:runtime-coordinator"] + end + + subgraph "Runtime Containers" + BASE["hca-runtime-base
OpenCode + Common Tools"] + RUST["hca-runtime-rust
:4100"] + TS["hca-runtime-typescript
:4101"] + PY["hca-runtime-python
:4102"] + GEN["hca-runtime-generic
:4103"] + INFRA["hca-runtime-infrastructure
:4104"] + end + + ROOT -->|"delegate"| COORD + COORD -->|"route by language"| RUST + COORD -->|"route by language"| TS + COORD -->|"route by language"| PY + COORD -->|"route by language"| GEN + COORD -->|"route by language"| INFRA + + BASE -.->|"FROM"| RUST + BASE -.->|"FROM"| TS + BASE -.->|"FROM"| PY + BASE -.->|"FROM"| GEN + BASE -.->|"FROM"| INFRA +``` + +### Runtime Coordinator Holon + +Domain-level holon that analyzes tasks and routes to appropriate runtime containers. + +```mermaid +graph LR + subgraph "Task Analysis" + EXT["File Extension
.rs .ts .py .md .yaml"] + MARKER["Project Markers
Cargo.toml, package.json"] + DESC["Task Description
Keywords"] + end + + subgraph "Routing Decision" + MATCH["Best Match"] + FALLBACK["Fallback: generic"] + end + + EXT --> MATCH + MARKER --> MATCH + DESC --> MATCH + MATCH --> FALLBACK +``` + +| Property | Value | +|----------|-------| +| Type | DOMAIN | +| Parent | holon:i2p:root | +| Can Delegate | Yes | +| Tools | template_parser, container_registry | + +**Runtime Selection Logic:** + +```typescript +const runtimeMapping = { + // File extensions + extensions: { + '.rs': 'rust', + '.ts': 'typescript', '.tsx': 'typescript', '.js': 'typescript', + '.py': 'python', + '.md': 'generic', '.mdx': 'generic', + '.yaml': 'infrastructure', '.yml': 'infrastructure' + }, + // Project markers (higher priority) + projectMarkers: { + 'Cargo.toml': 'rust', + 'package.json': 'typescript', + 'pyproject.toml': 'python', + 'Chart.yaml': 'infrastructure' + } +}; +``` + +### Container Registry + +| Runtime | Image | Port | Toolchain | +|---------|-------|------|-----------| +| Base | `ghcr.io/ardaglobal/hca-runtime-base` | - | OpenCode, ast-grep, ripgrep, jq | +| Rust | `ghcr.io/ardaglobal/hca-runtime-rust` | 4100 | cargo, rustc, clippy, cargo-nextest | +| TypeScript | `ghcr.io/ardaglobal/hca-runtime-typescript` | 4101 | bun, node, tsc, vitest, eslint | +| Python | `ghcr.io/ardaglobal/hca-runtime-python` | 4102 | python3, uv, pytest, ruff, mypy | +| Generic | `ghcr.io/ardaglobal/hca-runtime-generic` | 4103 | markdownlint, prettier, pandoc, vale | +| Infrastructure | `ghcr.io/ardaglobal/hca-runtime-infrastructure` | 4104 | kubectl, helm, helmfile, kustomize | + +### Skills Integration + +Each runtime container loads a SKILL.md that defines its capabilities: + +``` +/home/opencode/workspace/.opencode/skill//SKILL.md +``` + +Skills use YAML frontmatter: + +```yaml +--- +name: rust +description: Compile, test, and lint Rust projects +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Rust Skill +Instructions for working with Rust projects... +``` + +### Container Build Order + +```mermaid +graph LR + subgraph "Phase 1" + BASE["hca-runtime-base"] + SKILLS["SKILL.md templates"] + end + + subgraph "Phase 2" + RUST["rust"] + TS["typescript"] + PY["python"] + GEN["generic"] + INFRA["infrastructure"] + end + + subgraph "Phase 3" + COORD["RuntimeCoordinatorHolon"] + end + + BASE --> RUST + BASE --> TS + BASE --> PY + BASE --> GEN + BASE --> INFRA + SKILLS --> RUST + SKILLS --> TS + SKILLS --> PY + SKILLS --> GEN + SKILLS --> INFRA + RUST & TS & PY & GEN & INFRA --> COORD +``` + +## Issue Description Templates + +Standardized markdown template for machine-parseable task specifications. + +### Template Schema + +```markdown +## Overview +[Brief description] + +**Target Repository:** ardaglobal/ +**Working Directory:** `/` +**Image Name:** `ghcr.io/ardaglobal/:latest` +**Base Image:** `FROM ` + +## Dependencies +- **BLOCKER:** must be completed first +- **DEPENDS ON:** must be available +- **CONSUMED BY:** will use this + +## Required Components +### Section Name +- [ ] Component 1 +- [ ] Component 2 + +## Success Criteria +- [ ] Criterion 1 +- [ ] Criterion 2 +``` + +### Template Parser/Generator + +```typescript +interface ParsedIssue { + overview: string; + targetRepo: string; + workingDir: string; + imageName?: string; + baseImage?: string; + dependencies: Dependency[]; + components: Component[]; + successCriteria: string[]; +} + +// Parse existing issue +const parsed = parseIssueDescription(issue.description); + +// Generate new issue +const description = generateIssueDescription({ + template: 'container-task', + ...data +}); +``` + ## Artifacts System ```mermaid @@ -448,6 +717,7 @@ impl/mvp/ β”‚ β”‚ β”œβ”€β”€ root.ts # Root orchestrator (uses ADK) β”‚ β”‚ β”œβ”€β”€ cursor-reviewer.ts # PR review holon β”‚ β”‚ β”œβ”€β”€ deep-reviewer.ts # OpenCode deep reasoning holon +β”‚ β”‚ β”œβ”€β”€ runtime-coordinator.ts # Runtime routing holon (TODO) β”‚ β”‚ β”‚ β”‚ β”‚ └── adk/ # ADK-based agent implementations β”‚ β”‚ β”œβ”€β”€ index.ts # Public exports @@ -465,6 +735,17 @@ impl/mvp/ β”‚ β”‚ β”œβ”€β”€ opencode-client.ts # OpenCode SDK β”‚ β”‚ └── opencode-pool.ts # OpenCode worker pool manager β”‚ β”‚ +β”‚ β”œβ”€β”€ templates/ # Issue description templates (TODO) +β”‚ β”‚ β”œβ”€β”€ index.ts # Template exports +β”‚ β”‚ β”œβ”€β”€ parser.ts # Parse description β†’ structured +β”‚ β”‚ β”œβ”€β”€ generator.ts # Generate description from structured +β”‚ β”‚ β”œβ”€β”€ types.ts # Template interfaces +β”‚ β”‚ └── templates/ +β”‚ β”‚ β”œβ”€β”€ container-task.md +β”‚ β”‚ β”œβ”€β”€ holon-task.md +β”‚ β”‚ β”œβ”€β”€ feature-task.md +β”‚ β”‚ └── fix-task.md +β”‚ β”‚ β”‚ β”œβ”€β”€ artifacts/ β”‚ β”‚ β”œβ”€β”€ types.ts # Artifact interfaces β”‚ β”‚ β”œβ”€β”€ schemas.ts # Zod schemas @@ -480,8 +761,48 @@ impl/mvp/ β”‚ └── protocol/ β”‚ └── errors.ts # HCA error codes β”‚ +β”œβ”€β”€ docker/ +β”‚ β”œβ”€β”€ opencode/ # OpenCode reasoning worker +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ β”œβ”€β”€ entrypoint.sh +β”‚ β”‚ β”œβ”€β”€ opencode-config.json +β”‚ β”‚ └── README.md +β”‚ β”‚ +β”‚ └── runtimes/ # Runtime execution containers (TODO) +β”‚ β”œβ”€β”€ base/ # Common base image +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ β”œβ”€β”€ entrypoint.sh +β”‚ β”‚ └── opencode-config.json +β”‚ β”œβ”€β”€ rust/ +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ └── SKILL.md +β”‚ β”œβ”€β”€ typescript/ +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ └── SKILL.md +β”‚ β”œβ”€β”€ python/ +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ └── SKILL.md +β”‚ β”œβ”€β”€ generic/ +β”‚ β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”‚ └── SKILL.md +β”‚ └── infrastructure/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ └── SKILL.md +β”‚ +β”œβ”€β”€ .opencode/ # OpenCode skills & agents (TODO) +β”‚ β”œβ”€β”€ skill/ +β”‚ β”‚ β”œβ”€β”€ rust/SKILL.md +β”‚ β”‚ β”œβ”€β”€ typescript/SKILL.md +β”‚ β”‚ β”œβ”€β”€ python/SKILL.md +β”‚ β”‚ β”œβ”€β”€ generic/SKILL.md +β”‚ β”‚ └── infrastructure/SKILL.md +β”‚ └── agent/ +β”‚ β”œβ”€β”€ executor.md +β”‚ └── reviewer.md +β”‚ β”œβ”€β”€ config/ -β”‚ └── opencode-instances.yaml # OpenCode worker pool configuration +β”‚ β”œβ”€β”€ opencode-instances.yaml # OpenCode worker pool configuration +β”‚ └── runtime-containers.yaml # Runtime container registry (TODO) β”‚ β”œβ”€β”€ artifacts/ # Stored artifacts (gitignored) β”œβ”€β”€ package.json @@ -619,6 +940,8 @@ const deep = await deepReviewPR("https://github.com/org/repo/pull/123"); - **Knowledge Base**: Implement lesson storage and retrieval - **Checkpoint/Resume**: Add task persistence for long-running analysis - **Distributed Artifacts**: Replace file-based storage with distributed system +- **Container Auto-Scaling**: Scale runtime containers based on queue depth +- **Telemetry**: Centralized logging and cost tracking across containers --- diff --git a/impl/mvp/docker/opencode/README.md b/impl/mvp/docker/opencode/README.md index f28482c..6ed798c 100644 --- a/impl/mvp/docker/opencode/README.md +++ b/impl/mvp/docker/opencode/README.md @@ -2,36 +2,45 @@ OpenCode instances used by the HCA-I2P reasoning pool for extended thinking and complex analysis. +> **Note:** This container is for **reasoning-only** workers. For code execution with language-specific toolchains, see [Runtime Containers](../runtimes/). + ## Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ HCA-I2P System β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ OpenCode Worker Pool β”‚ β”‚ -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ -β”‚ β”‚ β”‚ PRIMARY β”‚ β”‚ BATCH β”‚ β”‚ PREMIUM β”‚ β”‚ β”‚ -β”‚ β”‚ β”‚ Sonnet 4 β”‚ β”‚ Sonnet 4 β”‚ β”‚ Opus 4.5 β”‚ β”‚ β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ OpenCode β”‚ β”‚ OpenCode β”‚ β”‚ OpenCode β”‚ - β”‚ :4096 β”‚ β”‚ :4097 β”‚ β”‚ :4098 β”‚ - β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ OpenRouter β”‚ - β”‚ (LLM API) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Reasoning Workers (this container) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ PRIMARY β”‚ β”‚ BATCH β”‚ β”‚ PREMIUM β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ :4096 β”‚ β”‚ :4097 β”‚ β”‚ :4098 β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Runtime Containers (../runtimes/) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Rust β”‚ β”‚ TS β”‚ β”‚ Py β”‚ β”‚ Doc β”‚ β”‚Infra β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚:4100 β”‚ β”‚:4101 β”‚ β”‚:4102 β”‚ β”‚:4103 β”‚ β”‚:4104 β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ OpenRouter β”‚ + β”‚ (LLM API) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` +## Container Types + +| Type | Purpose | Toolchain | Ports | +|------|---------|-----------|-------| +| **Reasoning** (this) | Analysis, planning, review | OpenCode only | 4096-4098 | +| **Runtime** | Code execution | OpenCode + language tools | 4100-4104 | + ## Quick Start ### Build diff --git a/impl/mvp/docker/runtimes/README.md b/impl/mvp/docker/runtimes/README.md new file mode 100644 index 0000000..3322206 --- /dev/null +++ b/impl/mvp/docker/runtimes/README.md @@ -0,0 +1,122 @@ +# Runtime Containers + +Language-specific execution containers for HCA autonomous code operations. + +Each container extends the common base image with OpenCode pre-configured for autonomous operation. + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ hca-runtime-base β”‚ +β”‚ OpenCode + ast-grep + ripgrep + jq + git β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Rust β”‚ β”‚ TS β”‚ β”‚ Python β”‚ β”‚Generic β”‚ β”‚ Infra β”‚ + β”‚ :4100 β”‚ β”‚ :4101 β”‚ β”‚ :4102 β”‚ β”‚ :4103 β”‚ β”‚ :4104 β”‚ + β”‚ cargo β”‚ β”‚ bun β”‚ β”‚ uv β”‚ β”‚ mdlint β”‚ β”‚ kubectlβ”‚ + β”‚ rustc β”‚ β”‚ node β”‚ β”‚ pytest β”‚ β”‚prettierβ”‚ β”‚ helm β”‚ + β”‚ clippy β”‚ β”‚ tsc β”‚ β”‚ ruff β”‚ β”‚ vale β”‚ β”‚kustomizβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Container Registry + +| Runtime | Image | Port | Status | +|---------|-------|------|--------| +| Base | `ghcr.io/ardaglobal/hca-runtime-base` | - | TODO | +| Rust | `ghcr.io/ardaglobal/hca-runtime-rust` | 4100 | TODO | +| TypeScript | `ghcr.io/ardaglobal/hca-runtime-typescript` | 4101 | TODO | +| Python | `ghcr.io/ardaglobal/hca-runtime-python` | 4102 | TODO | +| Generic | `ghcr.io/ardaglobal/hca-runtime-generic` | 4103 | TODO | +| Infrastructure | `ghcr.io/ardaglobal/hca-runtime-infrastructure` | 4104 | TODO | + +## Build Order + +```bash +# 1. Build base image first +docker build -t ghcr.io/ardaglobal/hca-runtime-base ./base + +# 2. Build runtime images (can be parallel) +docker build -t ghcr.io/ardaglobal/hca-runtime-rust ./rust +docker build -t ghcr.io/ardaglobal/hca-runtime-typescript ./typescript +docker build -t ghcr.io/ardaglobal/hca-runtime-python ./python +docker build -t ghcr.io/ardaglobal/hca-runtime-generic ./generic +docker build -t ghcr.io/ardaglobal/hca-runtime-infrastructure ./infrastructure +``` + +## Running Containers + +```bash +# Start all runtime containers +docker run -d --name hca-rust -p 4100:4096 \ + -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ + -e GITHUB_TOKEN=$GITHUB_TOKEN \ + ghcr.io/ardaglobal/hca-runtime-rust + +docker run -d --name hca-typescript -p 4101:4096 \ + -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ + -e GITHUB_TOKEN=$GITHUB_TOKEN \ + ghcr.io/ardaglobal/hca-runtime-typescript + +# ... etc for other runtimes +``` + +## Directory Structure + +``` +runtimes/ +β”œβ”€β”€ README.md # This file +β”œβ”€β”€ base/ # Common base image +β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”œβ”€β”€ entrypoint.sh +β”‚ └── opencode-config.json +β”œβ”€β”€ rust/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ └── SKILL.md +β”œβ”€β”€ typescript/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ └── SKILL.md +β”œβ”€β”€ python/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ └── SKILL.md +β”œβ”€β”€ generic/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ └── SKILL.md +└── infrastructure/ + β”œβ”€β”€ Dockerfile + └── SKILL.md +``` + +## Skills Integration + +Each runtime includes a SKILL.md that defines its capabilities for OpenCode: + +```yaml +--- +name: rust +description: Compile, test, and lint Rust projects +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Rust Skill +Instructions for working with Rust projects... +``` + +Skills are copied into containers at: +``` +/home/opencode/workspace/.opencode/skill//SKILL.md +``` + +## Related + +- [Reasoning Workers](../opencode/) - Analysis-only containers without execution toolchains +- [ARCHITECTURE.md](../../ARCHITECTURE.md) - Full system architecture +- [Linear: ARDGBL-1073](https://linear.app/ardaglobal/issue/ARDGBL-1073) - Runtime Execution Layer epic diff --git a/impl/mvp/docker/runtimes/base/Dockerfile b/impl/mvp/docker/runtimes/base/Dockerfile new file mode 100644 index 0000000..5d62cbf --- /dev/null +++ b/impl/mvp/docker/runtimes/base/Dockerfile @@ -0,0 +1,133 @@ +# HCA Runtime Base Image +# +# Common foundation for all runtime execution containers. +# Includes OpenCode + common tooling. Language runtimes extend this. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-base . +# +# ============================================================================ + +FROM debian:bookworm-slim + +# ============================================================================ +# COMMON TOOLING +# ============================================================================ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Core utilities + curl \ + ca-certificates \ + wget \ + unzip \ + zip \ + # Git for repo operations + git \ + openssh-client \ + # Build essentials (needed by some tools) + build-essential \ + # Python (many tools depend on it) + python3 \ + python3-pip \ + # Text processing + jq \ + # Search tools + ripgrep \ + fd-find \ + # Directory visualization + tree \ + # Locales for UTF-8 + locales \ + && rm -rf /var/lib/apt/lists/* \ + # Setup UTF-8 locale + && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \ + && locale-gen \ + # Create fd symlink (Debian names it fdfind) + && ln -s /usr/bin/fdfind /usr/local/bin/fd + +# ============================================================================ +# YQ (YAML processor - like jq for YAML) +# ============================================================================ + +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "arm64" ]; then YQ_ARCH="arm64"; else YQ_ARCH="amd64"; fi && \ + curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${YQ_ARCH}" -o /usr/local/bin/yq && \ + chmod +x /usr/local/bin/yq + +# ============================================================================ +# AST-GREP (code search/refactoring) +# ============================================================================ + +# Detect architecture and download appropriate binary +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "arm64" ]; then \ + curl -fsSL https://github.com/ast-grep/ast-grep/releases/latest/download/app-aarch64-unknown-linux-gnu.zip -o /tmp/ast-grep.zip; \ + else \ + curl -fsSL https://github.com/ast-grep/ast-grep/releases/latest/download/app-x86_64-unknown-linux-gnu.zip -o /tmp/ast-grep.zip; \ + fi && \ + unzip -o /tmp/ast-grep.zip -d /usr/local/bin && \ + chmod +x /usr/local/bin/ast-grep /usr/local/bin/sg && \ + rm /tmp/ast-grep.zip + +# ============================================================================ +# OPENCODE +# ============================================================================ + +RUN curl -fsSL https://opencode.ai/install | bash && \ + mv /root/.opencode/bin/opencode /usr/local/bin/opencode && \ + chmod +x /usr/local/bin/opencode + +# ============================================================================ +# USER SETUP +# ============================================================================ + +RUN useradd -m -u 1001 opencode && \ + mkdir -p /home/opencode/.local/share/opencode/log \ + /home/opencode/.config/opencode \ + /home/opencode/.cache/opencode \ + /home/opencode/workspace \ + /home/opencode/workspace/.opencode/skill && \ + chown -R opencode:opencode /home/opencode + +# ============================================================================ +# OPENCODE CONFIG (auto-approve for autonomous operation) +# ============================================================================ + +COPY --chown=opencode:opencode opencode-config.json /home/opencode/.config/opencode/opencode.json +RUN chmod 644 /home/opencode/.config/opencode/opencode.json + +USER opencode +WORKDIR /home/opencode/workspace + +# ============================================================================ +# ENVIRONMENT +# ============================================================================ + +ENV OPENCODE_DISABLE_AUTOUPDATE=true +ENV OPENCODE_DISABLE_TERMINAL_TITLE=true +ENV OPENCODE_CLIENT=api + +# UTF-8 locale +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 + +# Git config for commits +ENV GIT_AUTHOR_NAME="HCA Runtime" +ENV GIT_AUTHOR_EMAIL="runtime@hca.local" +ENV GIT_COMMITTER_NAME="HCA Runtime" +ENV GIT_COMMITTER_EMAIL="runtime@hca.local" + +# ============================================================================ +# ENTRYPOINT +# ============================================================================ + +COPY --chown=opencode:opencode entrypoint.sh /home/opencode/entrypoint.sh +RUN chmod +x /home/opencode/entrypoint.sh + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD wget -q --spider http://localhost:4096/global/health || exit 1 + +EXPOSE 4096 + +ENTRYPOINT ["/home/opencode/entrypoint.sh"] +CMD ["opencode", "serve", "--port", "4096", "--hostname", "0.0.0.0"] diff --git a/impl/mvp/docker/runtimes/base/entrypoint.sh b/impl/mvp/docker/runtimes/base/entrypoint.sh new file mode 100644 index 0000000..2545959 --- /dev/null +++ b/impl/mvp/docker/runtimes/base/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# OpenCode Worker Entrypoint +# Configures git credentials from GITHUB_TOKEN for repo access + +set -e + +# Configure git credential helper if GITHUB_TOKEN is provided +if [ -n "$GITHUB_TOKEN" ]; then + echo "[entrypoint] Configuring git credentials from GITHUB_TOKEN" + + # Use credential helper to inject token for all github.com requests + git config --global credential.helper store + echo "https://x-access-token:${GITHUB_TOKEN}@github.com" > ~/.git-credentials + chmod 600 ~/.git-credentials + + # Also set up for git:// protocol + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:" + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + + echo "[entrypoint] Git configured for authenticated access to github.com" +else + echo "[entrypoint] No GITHUB_TOKEN provided - only public repos accessible" +fi + +# Configure git defaults +git config --global init.defaultBranch main +git config --global pull.rebase false +git config --global core.autocrlf input + +# Write OpenCode config if provided via env var (overrides default) +if [ -n "$OPENCODE_CONFIG_CONTENT" ]; then + echo "[entrypoint] Overriding OpenCode config from OPENCODE_CONFIG_CONTENT" + echo "$OPENCODE_CONFIG_CONTENT" > ~/.config/opencode/opencode.json +else + echo "[entrypoint] Using default autonomous worker config" +fi + +echo "[entrypoint] Starting OpenCode worker..." +exec "$@" diff --git a/impl/mvp/docker/runtimes/base/opencode-config.json b/impl/mvp/docker/runtimes/base/opencode-config.json new file mode 100644 index 0000000..7ebe090 --- /dev/null +++ b/impl/mvp/docker/runtimes/base/opencode-config.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://opencode.ai/config.schema.json", + "enabled_providers": ["openrouter"], + "permission": { + "bash": "allow", + "read": "allow", + "edit": "allow", + "glob": "allow", + "grep": "allow", + "list": "allow", + "task": "allow", + "external_directory": { + "/tmp/*": "allow" + }, + "webfetch": "allow", + "websearch": "allow", + "codesearch": "allow" + } +} diff --git a/impl/mvp/docker/runtimes/generic/Dockerfile b/impl/mvp/docker/runtimes/generic/Dockerfile new file mode 100644 index 0000000..f532697 --- /dev/null +++ b/impl/mvp/docker/runtimes/generic/Dockerfile @@ -0,0 +1,54 @@ +# HCA Generic Runtime Container +# +# Extends base image with documentation and general-purpose tools. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-generic . +# +# Run: +# docker run -d --name hca-generic -p 4103:4096 \ +# -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ +# -e GITHUB_TOKEN=$GITHUB_TOKEN \ +# ghcr.io/ardaglobal/hca-runtime-generic +# +# ============================================================================ + +FROM hca-runtime-base:latest + +USER root + +# ============================================================================ +# DOCUMENTATION TOOLS +# ============================================================================ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + pandoc \ + aspell \ + aspell-en \ + && rm -rf /var/lib/apt/lists/* + +# Node.js for markdownlint/prettier +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +RUN npm install -g \ + markdownlint-cli \ + prettier + +# ============================================================================ +# SKILL DEFINITION +# ============================================================================ + +USER opencode + +COPY --chown=opencode:opencode SKILL.md /home/opencode/workspace/.opencode/skill/generic/SKILL.md + +# ============================================================================ +# LABELS +# ============================================================================ + +LABEL org.opencontainers.image.title="HCA Generic Runtime" +LABEL org.opencontainers.image.description="Documentation/general-purpose container for HCA" +LABEL hca.runtime="generic" +LABEL hca.port="4103" diff --git a/impl/mvp/docker/runtimes/generic/SKILL.md b/impl/mvp/docker/runtimes/generic/SKILL.md new file mode 100644 index 0000000..6631a6f --- /dev/null +++ b/impl/mvp/docker/runtimes/generic/SKILL.md @@ -0,0 +1,82 @@ +--- +name: generic +description: Write, edit, and lint documentation files +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Generic/Documentation Skill + +You are working in a documentation and general-purpose environment. + +## Available Tools + +- `markdownlint` - Markdown linter +- `prettier` - File formatter (markdown, yaml, json) +- `pandoc` - Document converter +- `aspell` - Spell checker + +## Project Detection + +This runtime handles: +- Markdown files (`.md`, `.mdx`) +- JSON files (`.json`) +- YAML files (`.yaml`, `.yml`) +- Documentation directories (`docs/`, `documentation/`) + +## Common Workflows + +### Lint Markdown +```bash +markdownlint "**/*.md" 2>&1 +``` + +### Format Files +```bash +# Markdown +prettier --write "**/*.md" 2>&1 + +# YAML +prettier --write "**/*.yaml" "**/*.yml" 2>&1 + +# JSON +prettier --write "**/*.json" 2>&1 +``` + +### Check Formatting +```bash +prettier --check . 2>&1 +``` + +### Spell Check +```bash +aspell check README.md +``` + +### Convert Documents +```bash +# Markdown to HTML +pandoc README.md -o README.html + +# Markdown to PDF (requires LaTeX) +pandoc README.md -o README.pdf +``` + +## Markdown Best Practices + +1. Use consistent heading levels +2. Add blank lines around code blocks +3. Use reference-style links for repeated URLs +4. Keep lines under 120 characters when possible + +## File Types Handled + +- `.md` - Markdown +- `.mdx` - MDX (Markdown + JSX) +- `.json` - JSON configuration +- `.yaml`/`.yml` - YAML configuration +- `.txt` - Plain text diff --git a/impl/mvp/docker/runtimes/infrastructure/Dockerfile b/impl/mvp/docker/runtimes/infrastructure/Dockerfile new file mode 100644 index 0000000..ce15a14 --- /dev/null +++ b/impl/mvp/docker/runtimes/infrastructure/Dockerfile @@ -0,0 +1,66 @@ +# HCA Infrastructure Runtime Container +# +# Extends base image with Kubernetes/Helm tooling. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-infrastructure . +# +# Run: +# docker run -d --name hca-infrastructure -p 4104:4096 \ +# -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ +# -e GITHUB_TOKEN=$GITHUB_TOKEN \ +# ghcr.io/ardaglobal/hca-runtime-infrastructure +# +# ============================================================================ + +FROM hca-runtime-base:latest + +USER root + +# ============================================================================ +# KUBECTL +# ============================================================================ + +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/$(dpkg --print-architecture)/kubectl" && \ + install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \ + rm kubectl + +# ============================================================================ +# HELM +# ============================================================================ + +RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# ============================================================================ +# HELMFILE +# ============================================================================ + +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "arm64" ]; then HF_ARCH="arm64"; else HF_ARCH="amd64"; fi && \ + HELMFILE_VERSION=$(curl -s https://api.github.com/repos/helmfile/helmfile/releases/latest | grep tag_name | cut -d '"' -f 4 | sed 's/v//') && \ + curl -fsSL "https://github.com/helmfile/helmfile/releases/download/v${HELMFILE_VERSION}/helmfile_${HELMFILE_VERSION}_linux_${HF_ARCH}.tar.gz" | tar xz -C /usr/local/bin helmfile && \ + chmod +x /usr/local/bin/helmfile + +# ============================================================================ +# KUSTOMIZE +# ============================================================================ + +RUN curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash && \ + mv kustomize /usr/local/bin/ + +# ============================================================================ +# SKILL DEFINITION +# ============================================================================ + +USER opencode + +COPY --chown=opencode:opencode SKILL.md /home/opencode/workspace/.opencode/skill/infrastructure/SKILL.md + +# ============================================================================ +# LABELS +# ============================================================================ + +LABEL org.opencontainers.image.title="HCA Infrastructure Runtime" +LABEL org.opencontainers.image.description="Kubernetes/Helm container for HCA" +LABEL hca.runtime="infrastructure" +LABEL hca.port="4104" diff --git a/impl/mvp/docker/runtimes/infrastructure/SKILL.md b/impl/mvp/docker/runtimes/infrastructure/SKILL.md new file mode 100644 index 0000000..1b0b4e9 --- /dev/null +++ b/impl/mvp/docker/runtimes/infrastructure/SKILL.md @@ -0,0 +1,105 @@ +--- +name: infrastructure +description: Manage Kubernetes resources and Helm charts +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Infrastructure Skill + +You are working in a Kubernetes/Infrastructure environment. + +## Available Tools + +- `kubectl` - Kubernetes CLI +- `helm` - Helm package manager +- `helmfile` - Declarative Helm +- `kustomize` - Kubernetes customization + +## Project Detection + +Look for these files to identify infrastructure projects: +- `Chart.yaml` - Helm chart +- `helmfile.yaml` - Helmfile config +- `kustomization.yaml` - Kustomize config +- `*.yaml` in `k8s/` or `kubernetes/` directories + +## Common Workflows + +### Helm Charts + +```bash +# Lint chart +helm lint ./chart 2>&1 + +# Template (dry-run) +helm template my-release ./chart 2>&1 + +# Package +helm package ./chart 2>&1 + +# Dependency update +helm dependency update ./chart 2>&1 +``` + +### Helmfile + +```bash +# Lint all releases +helmfile lint 2>&1 + +# Diff changes +helmfile diff 2>&1 + +# Template all +helmfile template 2>&1 +``` + +### Kustomize + +```bash +# Build manifests +kustomize build . 2>&1 + +# Build with specific overlay +kustomize build overlays/production 2>&1 +``` + +### Kubectl (Validation Only) + +```bash +# Validate manifests +kubectl apply --dry-run=client -f manifest.yaml 2>&1 + +# Validate with server +kubectl apply --dry-run=server -f manifest.yaml 2>&1 +``` + +## Validation + +```bash +# Validate YAML syntax +yq '.' manifest.yaml > /dev/null 2>&1 + +# Check Helm chart +helm lint ./chart --strict 2>&1 +``` + +## Best Practices + +1. Always lint Helm charts before committing +2. Use `--dry-run` for kubectl operations +3. Validate YAML syntax with `yq` +4. Check for hardcoded values that should be templated +5. Verify resource limits are set + +## Security Checks + +- No secrets in plain text +- Resource limits defined +- Security contexts set +- No `latest` image tags diff --git a/impl/mvp/docker/runtimes/python/Dockerfile b/impl/mvp/docker/runtimes/python/Dockerfile new file mode 100644 index 0000000..c1d0249 --- /dev/null +++ b/impl/mvp/docker/runtimes/python/Dockerfile @@ -0,0 +1,56 @@ +# HCA Python Runtime Container +# +# Extends base image with Python toolchain for running Python projects. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-python . +# +# Run: +# docker run -d --name hca-python -p 4102:4096 \ +# -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ +# -e GITHUB_TOKEN=$GITHUB_TOKEN \ +# ghcr.io/ardaglobal/hca-runtime-python +# +# ============================================================================ + +FROM hca-runtime-base:latest + +USER root + +# ============================================================================ +# UV (Fast Python package installer) +# ============================================================================ + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + chmod +x /usr/local/bin/uv + +# ============================================================================ +# PYTHON TOOLS (via pip, already have python3) +# ============================================================================ + +RUN pip3 install --break-system-packages \ + poetry \ + pytest \ + pytest-asyncio \ + ruff \ + mypy \ + black \ + isort + +# ============================================================================ +# SKILL DEFINITION +# ============================================================================ + +USER opencode + +COPY --chown=opencode:opencode SKILL.md /home/opencode/workspace/.opencode/skill/python/SKILL.md + +# ============================================================================ +# LABELS +# ============================================================================ + +LABEL org.opencontainers.image.title="HCA Python Runtime" +LABEL org.opencontainers.image.description="Python execution container for HCA" +LABEL hca.runtime="python" +LABEL hca.port="4102" diff --git a/impl/mvp/docker/runtimes/python/SKILL.md b/impl/mvp/docker/runtimes/python/SKILL.md new file mode 100644 index 0000000..d394baf --- /dev/null +++ b/impl/mvp/docker/runtimes/python/SKILL.md @@ -0,0 +1,100 @@ +--- +name: python +description: Run, test, and lint Python projects +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Python Development Skill + +You are working in a Python environment with modern tooling. + +## Available Tools + +- `python3` - Python interpreter +- `uv` - Fast package installer (preferred) +- `pip` - Standard package installer +- `poetry` - Dependency management + +## Project Detection + +Look for these files to identify Python projects: +- `pyproject.toml` - Modern project config +- `setup.py` - Legacy setup +- `requirements.txt` - Dependencies +- `poetry.lock` - Poetry lockfile +- `uv.lock` - UV lockfile + +## Common Workflows + +### Install Dependencies +```bash +# With uv (fastest) +uv pip install -r requirements.txt 2>&1 + +# With poetry +poetry install 2>&1 + +# With pip +pip install -r requirements.txt 2>&1 +``` + +### Run Tests +```bash +pytest 2>&1 +# or with verbose +pytest -v 2>&1 +``` + +### Lint +```bash +ruff check . 2>&1 +``` + +### Format +```bash +ruff format . 2>&1 +# or +black . 2>&1 +``` + +### Type Check +```bash +mypy . 2>&1 +``` + +### Import Sorting +```bash +isort . --check 2>&1 +``` + +## Poetry Projects + +```bash +poetry install # Install deps +poetry run pytest # Run tests +poetry run python main.py # Run script +``` + +## Virtual Environments + +```bash +# Create venv +python3 -m venv .venv +source .venv/bin/activate + +# Or with uv +uv venv +source .venv/bin/activate +``` + +## Best Practices + +1. Use `ruff` for fast linting (replaces flake8, isort, etc.) +2. Use `uv` for fast package installation +3. Always run type checks with `mypy` +4. Check for `pyproject.toml` vs `requirements.txt` diff --git a/impl/mvp/docker/runtimes/rust/Dockerfile b/impl/mvp/docker/runtimes/rust/Dockerfile new file mode 100644 index 0000000..3512bd1 --- /dev/null +++ b/impl/mvp/docker/runtimes/rust/Dockerfile @@ -0,0 +1,67 @@ +# HCA Rust Runtime Container +# +# Extends base image with Rust toolchain for compiling and testing Rust projects. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-rust . +# +# Run: +# docker run -d --name hca-rust -p 4100:4096 \ +# -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ +# -e GITHUB_TOKEN=$GITHUB_TOKEN \ +# ghcr.io/ardaglobal/hca-runtime-rust +# +# ============================================================================ + +FROM hca-runtime-base:latest + +USER root + +# ============================================================================ +# NATIVE DEPENDENCIES (for Arda Rust repos) +# ============================================================================ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + # PostgreSQL client (for sqlx) + libpq-dev \ + # OpenSSL (for TLS/crypto) + libssl-dev \ + pkg-config \ + # Protobuf compiler (for prost/tonic if needed) + protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* + +# ============================================================================ +# RUST TOOLCHAIN +# ============================================================================ + +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH=/usr/local/cargo/bin:$PATH + +# Install Rust via rustup (system-wide) +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --default-toolchain stable --no-modify-path && \ + chmod -R a+w $RUSTUP_HOME $CARGO_HOME + +# Install Rust components and tools +RUN rustup component add rustfmt clippy && \ + cargo install cargo-nextest cargo-watch sqlx-cli --locked + +# ============================================================================ +# SKILL DEFINITION +# ============================================================================ + +USER opencode + +# Copy skill definition +COPY --chown=opencode:opencode SKILL.md /home/opencode/workspace/.opencode/skill/rust/SKILL.md + +# ============================================================================ +# LABELS +# ============================================================================ + +LABEL org.opencontainers.image.title="HCA Rust Runtime" +LABEL org.opencontainers.image.description="Rust execution container for HCA" +LABEL hca.runtime="rust" +LABEL hca.port="4100" diff --git a/impl/mvp/docker/runtimes/rust/SKILL.md b/impl/mvp/docker/runtimes/rust/SKILL.md new file mode 100644 index 0000000..43d9a94 --- /dev/null +++ b/impl/mvp/docker/runtimes/rust/SKILL.md @@ -0,0 +1,71 @@ +--- +name: rust +description: Compile, test, and lint Rust projects +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# Rust Development Skill + +You are working in a Rust development environment with full toolchain access. + +## Available Tools + +- `cargo build` - Compile the project +- `cargo build --release` - Compile with optimizations +- `cargo test` - Run tests +- `cargo nextest run` - Run tests with better output (preferred) +- `cargo clippy` - Run linter +- `cargo fmt` - Format code +- `cargo check` - Fast type checking without building +- `cargo doc` - Generate documentation +- `cargo watch -x test` - Watch mode for tests + +## Project Detection + +Look for these files to identify Rust projects: +- `Cargo.toml` - Project manifest +- `Cargo.lock` - Dependency lock file +- `src/main.rs` - Binary entry point +- `src/lib.rs` - Library entry point + +## Common Workflows + +### Building +```bash +cargo build 2>&1 +``` + +### Running Tests +```bash +cargo nextest run 2>&1 +# or if nextest not available +cargo test 2>&1 +``` + +### Linting +```bash +cargo clippy -- -D warnings 2>&1 +``` + +### Formatting Check +```bash +cargo fmt --check 2>&1 +``` + +## Error Handling + +- Compilation errors show file:line:column format +- Use `cargo check` for faster feedback than full build +- Clippy warnings should be treated as errors with `-D warnings` + +## Best Practices + +1. Always run `cargo fmt --check` before committing +2. Run `cargo clippy` to catch common mistakes +3. Use `cargo nextest` for better test output +4. Check `Cargo.lock` changes for dependency updates diff --git a/impl/mvp/docker/runtimes/typescript/Dockerfile b/impl/mvp/docker/runtimes/typescript/Dockerfile new file mode 100644 index 0000000..47b4ca2 --- /dev/null +++ b/impl/mvp/docker/runtimes/typescript/Dockerfile @@ -0,0 +1,69 @@ +# HCA TypeScript Runtime Container +# +# Extends base image with Node.js/Bun for building TypeScript/React projects. +# +# Build: +# docker build -t ghcr.io/ardaglobal/hca-runtime-typescript . +# +# Run: +# docker run -d --name hca-typescript -p 4101:4096 \ +# -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \ +# -e GITHUB_TOKEN=$GITHUB_TOKEN \ +# ghcr.io/ardaglobal/hca-runtime-typescript +# +# ============================================================================ + +FROM hca-runtime-base:latest + +USER root + +# ============================================================================ +# BUN (Primary runtime - fast) +# ============================================================================ + +RUN curl -fsSL https://bun.sh/install | bash && \ + mv /root/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun + +# ============================================================================ +# NODE.JS LTS (Fallback for npm compatibility) +# ============================================================================ + +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +# ============================================================================ +# PACKAGE MANAGERS +# ============================================================================ + +RUN npm install -g pnpm + +# ============================================================================ +# GLOBAL TOOLS +# ============================================================================ + +RUN npm install -g \ + typescript \ + tsx \ + vitest \ + eslint \ + prettier \ + @biomejs/biome + +# ============================================================================ +# SKILL DEFINITION +# ============================================================================ + +USER opencode + +COPY --chown=opencode:opencode SKILL.md /home/opencode/workspace/.opencode/skill/typescript/SKILL.md + +# ============================================================================ +# LABELS +# ============================================================================ + +LABEL org.opencontainers.image.title="HCA TypeScript Runtime" +LABEL org.opencontainers.image.description="TypeScript/React execution container for HCA" +LABEL hca.runtime="typescript" +LABEL hca.port="4101" diff --git a/impl/mvp/docker/runtimes/typescript/SKILL.md b/impl/mvp/docker/runtimes/typescript/SKILL.md new file mode 100644 index 0000000..3be49de --- /dev/null +++ b/impl/mvp/docker/runtimes/typescript/SKILL.md @@ -0,0 +1,105 @@ +--- +name: typescript +description: Build, test, and lint TypeScript/React projects +allowed_tools: + - bash + - read + - edit + - glob + - grep +--- + +# TypeScript Development Skill + +You are working in a TypeScript/JavaScript environment with Node.js and Bun. + +## Available Runtimes + +- `bun` - Fast JavaScript runtime (preferred) +- `node` - Node.js LTS +- `tsx` - TypeScript execution + +## Package Managers + +- `bun install` - Fastest (preferred) +- `pnpm install` - Fast, disk efficient +- `npm install` - Standard + +## Project Detection + +Look for these files to identify TypeScript projects: +- `package.json` - Project manifest +- `tsconfig.json` - TypeScript config +- `vite.config.ts` - Vite build config +- `next.config.js` - Next.js config +- `bun.lockb` or `pnpm-lock.yaml` or `package-lock.json` + +## Common Workflows + +### Install Dependencies +```bash +# Prefer bun, fallback to pnpm/npm +bun install 2>&1 || pnpm install 2>&1 || npm install 2>&1 +``` + +### Type Check +```bash +bun run tsc --noEmit 2>&1 +# or +npx tsc --noEmit 2>&1 +``` + +### Run Tests +```bash +bun test 2>&1 +# or with vitest +bun run vitest run 2>&1 +``` + +### Build +```bash +bun run build 2>&1 +``` + +### Lint +```bash +bun run lint 2>&1 +# or directly +eslint . 2>&1 +``` + +### Format Check +```bash +prettier --check . 2>&1 +# or with biome +biome check . 2>&1 +``` + +## Framework-Specific + +### Vite Projects +```bash +bun run dev # Development server +bun run build # Production build +bun run preview # Preview production build +``` + +### Next.js Projects +```bash +bun run dev # Development +bun run build # Build +bun run start # Production server +``` + +## Error Handling + +- TypeScript errors show file:line:column format +- ESLint errors include rule names +- Check `node_modules` exists before running scripts + +## Best Practices + +1. Always run type check before committing +2. Use `bun` for faster execution when possible +3. Check for lockfile type to determine package manager +4. Run `lint` and `format` before commits From fff9498128355511fcc953ea69d90070b5ed25fa Mon Sep 17 00:00:00 2001 From: aWN4Y25pa2EK <19519604+aWN4Y25pa2EK@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:37:50 +0000 Subject: [PATCH 2/4] fix: resolve eslint errors and format code - Fix unnecessary regex escape characters in cursor-reviewer.ts - Fix unnecessary regex escape characters in github-client.ts - Run prettier on all TypeScript files Co-Authored-By: Claude Opus 4.5 --- impl/mvp/src/api/handlers.ts | 14 ++- impl/mvp/src/artifacts/schemas.ts | 10 +- impl/mvp/src/config.ts | 13 ++- impl/mvp/src/holons/adk/agents.ts | 7 +- impl/mvp/src/holons/adk/openrouter-llm.ts | 27 +++-- impl/mvp/src/holons/adk/tools.ts | 26 +++-- impl/mvp/src/holons/cursor-reviewer.ts | 118 +++++++++++++++------ impl/mvp/src/holons/deep-reviewer.ts | 11 +- impl/mvp/src/holons/root.ts | 17 ++- impl/mvp/src/holons/types.ts | 3 +- impl/mvp/src/tools/github-client.ts | 26 +++-- impl/mvp/src/tools/linear-client.ts | 29 ++++-- impl/mvp/src/tools/linear-watcher.ts | 4 +- impl/mvp/src/tools/opencode-client.ts | 13 ++- impl/mvp/src/tools/opencode-pool.ts | 120 ++++++++++++++-------- 15 files changed, 302 insertions(+), 136 deletions(-) diff --git a/impl/mvp/src/api/handlers.ts b/impl/mvp/src/api/handlers.ts index b047956..1ac243e 100644 --- a/impl/mvp/src/api/handlers.ts +++ b/impl/mvp/src/api/handlers.ts @@ -763,10 +763,15 @@ export function handleReviewStatus(requestContext: RequestContext) { const stats = { total: subtasks.length, pending: subtasks.filter((s) => s.status === ReviewStatus.PENDING).length, - review_pending: subtasks.filter((s) => s.status === ReviewStatus.REVIEW_PENDING).length, + review_pending: subtasks.filter( + (s) => s.status === ReviewStatus.REVIEW_PENDING, + ).length, approved: subtasks.filter((s) => s.status === ReviewStatus.APPROVED).length, - changes_requested: subtasks.filter((s) => s.status === ReviewStatus.CHANGES_REQUESTED).length, - escalated: subtasks.filter((s) => s.status === ReviewStatus.ESCALATED).length, + changes_requested: subtasks.filter( + (s) => s.status === ReviewStatus.CHANGES_REQUESTED, + ).length, + escalated: subtasks.filter((s) => s.status === ReviewStatus.ESCALATED) + .length, }; return { @@ -867,7 +872,8 @@ export async function handleReviewSubtask( status: subtask.status, prUrl: subtask.prUrl, }, - error: "Review did not produce a result (subtask may already be approved or escalated)", + error: + "Review did not produce a result (subtask may already be approved or escalated)", }; } diff --git a/impl/mvp/src/artifacts/schemas.ts b/impl/mvp/src/artifacts/schemas.ts index e7579c4..afdf68c 100644 --- a/impl/mvp/src/artifacts/schemas.ts +++ b/impl/mvp/src/artifacts/schemas.ts @@ -642,10 +642,12 @@ export const ExecutionResultSchema = z.object({ export const ReviewVerdictSchema = z.object({ approved: z.boolean(), criteriaResults: z.array(CriterionEvaluationSchema), - executionResults: z.object({ - typecheck: ExecutionResultSchema, - tests: ExecutionResultSchema, - }).optional(), // Optional for backward compatibility + executionResults: z + .object({ + typecheck: ExecutionResultSchema, + tests: ExecutionResultSchema, + }) + .optional(), // Optional for backward compatibility securityIssues: z.array(z.string()), summary: z.string().min(1), feedback: z.string(), diff --git a/impl/mvp/src/config.ts b/impl/mvp/src/config.ts index e6b7c4b..71c8b51 100644 --- a/impl/mvp/src/config.ts +++ b/impl/mvp/src/config.ts @@ -198,9 +198,16 @@ export function loadConfig(): Config { ), }, opencode: { - url: process.env["HCA_OPENCODE_URL"] ?? process.env["OPENCODE_URL"] ?? "https://oc1.dev.arda.xyz", - enabled: Boolean(process.env["HCA_OPENCODE_URL"] ?? process.env["OPENCODE_URL"]), - defaultModel: process.env["HCA_OPENCODE_MODEL"] ?? "openrouter/anthropic/claude-opus-4.5", + url: + process.env["HCA_OPENCODE_URL"] ?? + process.env["OPENCODE_URL"] ?? + "https://oc1.dev.arda.xyz", + enabled: Boolean( + process.env["HCA_OPENCODE_URL"] ?? process.env["OPENCODE_URL"], + ), + defaultModel: + process.env["HCA_OPENCODE_MODEL"] ?? + "openrouter/anthropic/claude-opus-4.5", }, log: { level: process.env["HCA_LOG_LEVEL"] ?? process.env["LOG_LEVEL"], diff --git a/impl/mvp/src/holons/adk/agents.ts b/impl/mvp/src/holons/adk/agents.ts index f9e866a..9895cbe 100644 --- a/impl/mvp/src/holons/adk/agents.ts +++ b/impl/mvp/src/holons/adk/agents.ts @@ -256,7 +256,12 @@ export const rootAgent = new LlmAgent({ new AgentTool({ agent: cursorReviewerAgent }), new AgentTool({ agent: deepReviewerAgent }), ], - subAgents: [architectAgent, reviewerAgent, cursorReviewerAgent, deepReviewerAgent], + subAgents: [ + architectAgent, + reviewerAgent, + cursorReviewerAgent, + deepReviewerAgent, + ], generateContentConfig: { maxOutputTokens: 4000, temperature: 0.5, diff --git a/impl/mvp/src/holons/adk/openrouter-llm.ts b/impl/mvp/src/holons/adk/openrouter-llm.ts index f0cf813..0256230 100644 --- a/impl/mvp/src/holons/adk/openrouter-llm.ts +++ b/impl/mvp/src/holons/adk/openrouter-llm.ts @@ -56,7 +56,10 @@ interface OpenAIResponse { /** * Convert Gemini Content[] to OpenAI messages */ -function geminiToOpenAI(contents: Content[], systemInstruction?: string): OpenAIMessage[] { +function geminiToOpenAI( + contents: Content[], + systemInstruction?: string, +): OpenAIMessage[] { const messages: OpenAIMessage[] = []; // Add system instruction if present @@ -65,7 +68,10 @@ function geminiToOpenAI(contents: Content[], systemInstruction?: string): OpenAI } for (const content of contents) { - const role = content.role === "model" ? "assistant" : content.role as "user" | "system"; + const role = + content.role === "model" + ? "assistant" + : (content.role as "user" | "system"); // Handle function responses if (content.parts?.some((p: Part) => "functionResponse" in p)) { @@ -74,7 +80,8 @@ function geminiToOpenAI(contents: Content[], systemInstruction?: string): OpenAI messages.push({ role: "tool", content: JSON.stringify(part.functionResponse.response), - tool_call_id: part.functionResponse.id || part.functionResponse.name, + tool_call_id: + part.functionResponse.id || part.functionResponse.name, }); } } @@ -175,7 +182,7 @@ function convertTools(tools?: any[]): any[] | undefined { description: fd.description, parameters: fd.parameters, }, - })) + })), ); } @@ -224,7 +231,7 @@ export class OpenRouterLlm extends BaseLlm { if (!this.apiKey) { throw new Error( - "OpenRouter API key must be set via OPENROUTER_API_KEY environment variable" + "OpenRouter API key must be set via OPENROUTER_API_KEY environment variable", ); } } @@ -234,7 +241,7 @@ export class OpenRouterLlm extends BaseLlm { */ async *generateContentAsync( llmRequest: LlmRequest, - _stream: boolean = false + _stream: boolean = false, ): AsyncGenerator { // Prepare the request this.maybeAppendUserContent(llmRequest); @@ -264,7 +271,7 @@ export class OpenRouterLlm extends BaseLlm { } console.log( - `[openrouter-llm] Request to ${requestBody.model}, messages: ${messages.length}, tools: ${tools?.length || 0}` + `[openrouter-llm] Request to ${requestBody.model}, messages: ${messages.length}, tools: ${tools?.length || 0}`, ); // For now, non-streaming only (streaming would need SSE parsing) @@ -281,14 +288,16 @@ export class OpenRouterLlm extends BaseLlm { if (!response.ok) { const errorText = await response.text(); - throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`); + throw new Error( + `OpenRouter API error: ${response.status} - ${errorText}`, + ); } const data = (await response.json()) as OpenAIResponse; const llmResponse = openAIToGemini(data); console.log( - `[openrouter-llm] Response from ${data.model}, tokens: ${data.usage?.total_tokens || "?"}` + `[openrouter-llm] Response from ${data.model}, tokens: ${data.usage?.total_tokens || "?"}`, ); yield llmResponse; diff --git a/impl/mvp/src/holons/adk/tools.ts b/impl/mvp/src/holons/adk/tools.ts index a9206f4..b62defd 100644 --- a/impl/mvp/src/holons/adk/tools.ts +++ b/impl/mvp/src/holons/adk/tools.ts @@ -150,7 +150,8 @@ export const llmInvokeTool = new FunctionTool({ */ export const linearFetchIssueTool = new FunctionTool({ name: "linear_fetch_issue", - description: "Fetch details of a Linear issue by ID or identifier (e.g., ARDGBL-1033)", + description: + "Fetch details of a Linear issue by ID or identifier (e.g., ARDGBL-1033)", parameters: z.object({ issue_id: z .string() @@ -209,7 +210,14 @@ export const linearCreateSubIssueTool = new FunctionTool({ .default(2) .describe("Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)"), }), - execute: async ({ parent_id, team_id, title, description, estimate, priority }) => { + execute: async ({ + parent_id, + team_id, + title, + description, + estimate, + priority, + }) => { const client = getLinearClient(); const result = await client.createSubIssue({ parentId: parent_id, @@ -321,7 +329,9 @@ export const githubApprovePRTool = new FunctionTool({ description: "Approve a GitHub PR with a review comment", parameters: z.object({ url: z.string().url().describe("GitHub PR URL"), - comment: z.string().describe("Approval comment explaining why PR is approved"), + comment: z + .string() + .describe("Approval comment explaining why PR is approved"), }), execute: async ({ url, comment }) => { const client = getGitHubClient(); @@ -421,18 +431,12 @@ export const rootHolonTools = [ /** * Tools for Architect Holon (analysis) */ -export const architectHolonTools = [ - vectorSearchTool, - llmInvokeTool, -]; +export const architectHolonTools = [vectorSearchTool, llmInvokeTool]; /** * Tools for Reviewer Holon (quality assurance) */ -export const reviewerHolonTools = [ - vectorSearchTool, - llmInvokeTool, -]; +export const reviewerHolonTools = [vectorSearchTool, llmInvokeTool]; /** * Tools for Cursor Reviewer (PR review) diff --git a/impl/mvp/src/holons/cursor-reviewer.ts b/impl/mvp/src/holons/cursor-reviewer.ts index ec7f542..72614ea 100644 --- a/impl/mvp/src/holons/cursor-reviewer.ts +++ b/impl/mvp/src/holons/cursor-reviewer.ts @@ -6,7 +6,12 @@ * evaluates PRs against acceptance criteria, and posts verdicts. */ -import { CURSOR_REVIEWER_HOLON_CARD, type HolonCard, HolonState, type HolonStateValue } from "./types.js"; +import { + CURSOR_REVIEWER_HOLON_CARD, + type HolonCard, + HolonState, + type HolonStateValue, +} from "./types.js"; import type { TrackedSubtask, ReviewVerdict, @@ -174,7 +179,9 @@ export class CursorReviewerHolon { const subtask = this.tracked.get(issueId); if (subtask) { subtask.status = status; - console.log(`[cursor-reviewer] Updated ${subtask.identifier} status to ${status}`); + console.log( + `[cursor-reviewer] Updated ${subtask.identifier} status to ${status}`, + ); } } @@ -253,9 +260,8 @@ export class CursorReviewerHolon { } // First check attachments (preferred - Cursor adds PR as attachment) - const issueWithAttachments = await this.linearClient.fetchIssueWithAttachments( - subtask.linearIssueId, - ); + const issueWithAttachments = + await this.linearClient.fetchIssueWithAttachments(subtask.linearIssueId); if (issueWithAttachments?.attachments?.nodes) { for (const attachment of issueWithAttachments.attachments.nodes) { // Check for GitHub PR URL in attachment @@ -272,7 +278,9 @@ export class CursorReviewerHolon { } // Fallback: check comments for Cursor completion - const comments = await this.linearClient.fetchComments(subtask.linearIssueId); + const comments = await this.linearClient.fetchComments( + subtask.linearIssueId, + ); // Look for Cursor's completion comment const cursorComment = comments.find((c) => { @@ -297,7 +305,7 @@ export class CursorReviewerHolon { // Extract PR URL const prMatch = cursorComment.body.match( - /https:\/\/github\.com\/[^\/]+\/[^\/]+\/pull\/\d+/, + /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/, ); return prMatch ? { prUrl: prMatch[0] } : null; } @@ -311,7 +319,9 @@ export class CursorReviewerHolon { throw new Error(`No PR URL for subtask ${subtask.identifier}`); } - console.log(`[cursor-reviewer] Reviewing PR via OpenCode (full execution): ${subtask.prUrl}`); + console.log( + `[cursor-reviewer] Reviewing PR via OpenCode (full execution): ${subtask.prUrl}`, + ); // Parse PR URL to extract repo info const parsed = GitHubClient.parsePRUrl(subtask.prUrl); @@ -321,7 +331,11 @@ export class CursorReviewerHolon { const repoFullName = `${parsed.owner}/${parsed.repo}`; // Fetch PR details (just metadata, OpenCode will fetch the actual code) - const pr = await this.githubClient.fetchPR(parsed.owner, parsed.repo, parsed.number); + const pr = await this.githubClient.fetchPR( + parsed.owner, + parsed.repo, + parsed.number, + ); // Build prompt for OpenCode to EXECUTE the review const userPrompt = buildReviewPrompt( @@ -342,7 +356,9 @@ export class CursorReviewerHolon { timeout: 300000, // 5 min timeout for full execution }); - console.log(`[cursor-reviewer] OpenCode review completed in ${response.durationMs}ms, cost: $${response.cost.toFixed(4)}`); + console.log( + `[cursor-reviewer] OpenCode review completed in ${response.durationMs}ms, cost: $${response.cost.toFixed(4)}`, + ); // Parse verdict from OpenCode response let verdict: ReviewVerdict; @@ -355,7 +371,10 @@ export class CursorReviewerHolon { const parsed = JSON.parse(jsonMatch[0]); verdict = ReviewVerdictSchema.parse(parsed); } catch (error) { - console.error("[cursor-reviewer] Failed to parse OpenCode response:", response.result.slice(0, 500)); + console.error( + "[cursor-reviewer] Failed to parse OpenCode response:", + response.result.slice(0, 500), + ); throw new Error(`Invalid review verdict: ${error}`); } @@ -366,7 +385,10 @@ export class CursorReviewerHolon { const usage: LLMUsage = { prompt_tokens: response.tokens.input, completion_tokens: response.tokens.output + response.tokens.reasoning, - total_tokens: response.tokens.input + response.tokens.output + response.tokens.reasoning, + total_tokens: + response.tokens.input + + response.tokens.output + + response.tokens.reasoning, }; return { @@ -406,9 +428,14 @@ export class CursorReviewerHolon { true, subtask.reviewIterations, undefined, - githubSuccess ? undefined : "(GitHub review failed - see PR for manual approval)", + githubSuccess + ? undefined + : "(GitHub review failed - see PR for manual approval)", + ); + await this.linearClient.createComment( + subtask.linearIssueId, + linearComment, ); - await this.linearClient.createComment(subtask.linearIssueId, linearComment); // Update Linear issue state to Done const issue = await this.linearClient.fetchIssue(subtask.linearIssueId); @@ -417,12 +444,12 @@ export class CursorReviewerHolon { } subtask.status = ReviewStatus.APPROVED; - console.log(`[cursor-reviewer] Approved ${subtask.identifier}${githubSuccess ? "" : " (Linear only)"}`); - + console.log( + `[cursor-reviewer] Approved ${subtask.identifier}${githubSuccess ? "" : " (Linear only)"}`, + ); } else if (subtask.reviewIterations >= subtask.maxIterations) { // Escalate - max iterations reached await this.escalate(subtask, verdict); - } else { // Try to request changes on GitHub (non-blocking) try { @@ -441,7 +468,10 @@ export class CursorReviewerHolon { subtask.reviewIterations, subtask.maxIterations, ); - await this.linearClient.createComment(subtask.linearIssueId, linearComment); + await this.linearClient.createComment( + subtask.linearIssueId, + linearComment, + ); subtask.status = ReviewStatus.CHANGES_REQUESTED; console.log( @@ -457,7 +487,9 @@ export class CursorReviewerHolon { subtask: TrackedSubtask, verdict: ReviewVerdict, ): Promise { - console.log(`[cursor-reviewer] Escalating ${subtask.identifier} - max iterations reached`); + console.log( + `[cursor-reviewer] Escalating ${subtask.identifier} - max iterations reached`, + ); // Post escalation comment to Linear const escalationComment = `## ⚠️ HCA Review: Escalated @@ -481,7 +513,10 @@ ${verdict.criteriaResults --- *Escalated by HCA-I2P Cursor Reviewer Holon*`; - await this.linearClient.createComment(subtask.linearIssueId, escalationComment); + await this.linearClient.createComment( + subtask.linearIssueId, + escalationComment, + ); // Add escalation label if possible const issue = await this.linearClient.fetchIssue(subtask.linearIssueId); @@ -539,14 +574,19 @@ ${!approved ? `\n### Feedback\n${verdict.feedback}` : ""} decompositionArtifactId: string = "imported", maxIterations: number = 3, ): Promise { - console.log(`[cursor-reviewer] Importing subtasks from parent: ${parentIssueId}`); + console.log( + `[cursor-reviewer] Importing subtasks from parent: ${parentIssueId}`, + ); const children = await this.linearClient.fetchChildren(parentIssueId); const subtasks: TrackedSubtask[] = []; for (const child of children) { // Skip completed/cancelled issues - if (child.state.type === "completed" || child.state.type === "cancelled") { + if ( + child.state.type === "completed" || + child.state.type === "cancelled" + ) { console.log( `[cursor-reviewer] Skipping ${child.identifier} (state: ${child.state.name})`, ); @@ -613,21 +653,21 @@ ${!approved ? `\n### Feedback\n${verdict.feedback}` : ""} ); if (acMatch) { // Extract bullet points - const bullets = acMatch[0].match(/^[\*\-]\s+(.+)$/gm); + const bullets = acMatch[0].match(/^[*-]\s+(.+)$/gm); if (bullets) { for (const bullet of bullets) { - criteria.push(bullet.replace(/^[\*\-]\s+/, "").trim()); + criteria.push(bullet.replace(/^[*-]\s+/, "").trim()); } } } // Fallback: look for any bullet list if (criteria.length === 0) { - const bullets = description.match(/^[\*\-]\s+(.+)$/gm); + const bullets = description.match(/^[*-]\s+(.+)$/gm); if (bullets) { for (const bullet of bullets.slice(0, 5)) { // Max 5 criteria - criteria.push(bullet.replace(/^[\*\-]\s+/, "").trim()); + criteria.push(bullet.replace(/^[*-]\s+/, "").trim()); } } } @@ -666,7 +706,9 @@ ${!approved ? `\n### Feedback\n${verdict.feedback}` : ""} if (assigned) { subtask.assignee = "Cursor"; subtask.status = ReviewStatus.DELEGATED; - console.log(`[cursor-reviewer] Delegated ${subtask.identifier} to Cursor`); + console.log( + `[cursor-reviewer] Delegated ${subtask.identifier} to Cursor`, + ); } return assigned; @@ -675,7 +717,10 @@ ${!approved ? `\n### Feedback\n${verdict.feedback}` : ""} /** * Delegate all pending subtasks to Cursor */ - async delegateAllToCursor(): Promise<{ delegated: string[]; failed: string[] }> { + async delegateAllToCursor(): Promise<{ + delegated: string[]; + failed: string[]; + }> { const delegated: string[] = []; const failed: string[] = []; @@ -725,11 +770,18 @@ ${!approved ? `\n### Feedback\n${verdict.feedback}` : ""} return { total: subtasks.length, pending: subtasks.filter((s) => s.status === ReviewStatus.PENDING).length, - delegated: subtasks.filter((s) => s.status === ReviewStatus.DELEGATED).length, - reviewPending: subtasks.filter((s) => s.status === ReviewStatus.REVIEW_PENDING).length, - approved: subtasks.filter((s) => s.status === ReviewStatus.APPROVED).length, - changesRequested: subtasks.filter((s) => s.status === ReviewStatus.CHANGES_REQUESTED).length, - escalated: subtasks.filter((s) => s.status === ReviewStatus.ESCALATED).length, + delegated: subtasks.filter((s) => s.status === ReviewStatus.DELEGATED) + .length, + reviewPending: subtasks.filter( + (s) => s.status === ReviewStatus.REVIEW_PENDING, + ).length, + approved: subtasks.filter((s) => s.status === ReviewStatus.APPROVED) + .length, + changesRequested: subtasks.filter( + (s) => s.status === ReviewStatus.CHANGES_REQUESTED, + ).length, + escalated: subtasks.filter((s) => s.status === ReviewStatus.ESCALATED) + .length, complete: this.isComplete(), }; } diff --git a/impl/mvp/src/holons/deep-reviewer.ts b/impl/mvp/src/holons/deep-reviewer.ts index 93e1112..02a39d4 100644 --- a/impl/mvp/src/holons/deep-reviewer.ts +++ b/impl/mvp/src/holons/deep-reviewer.ts @@ -8,7 +8,12 @@ * - ACP protocol compatibility */ -import { DEEP_REVIEWER_HOLON_CARD, type HolonCard, HolonState, type HolonStateValue } from "./types.js"; +import { + DEEP_REVIEWER_HOLON_CARD, + type HolonCard, + HolonState, + type HolonStateValue, +} from "./types.js"; import type { ReviewVerdict, TrackedSubtask } from "../artifacts/types.js"; import { ReviewVerdictSchema } from "../artifacts/schemas.js"; import { getGitHubClient } from "../tools/github-client.js"; @@ -144,7 +149,9 @@ export class DeepReviewerHolon { } // Fetch PR diff for context - const { pr, diff } = await this.githubClient.fetchPRFromUrl(request.prUrl); + const { pr, diff } = await this.githubClient.fetchPRFromUrl( + request.prUrl, + ); // Build the review prompt const prompt = this.buildReviewPrompt(request, pr, diff); diff --git a/impl/mvp/src/holons/root.ts b/impl/mvp/src/holons/root.ts index 6b995af..3c99411 100644 --- a/impl/mvp/src/holons/root.ts +++ b/impl/mvp/src/holons/root.ts @@ -8,7 +8,12 @@ import type { Task } from "../tasks/types.js"; import type { LinearSyncResult, SyncError } from "../artifacts/types.js"; import { SyncStatus } from "../artifacts/types.js"; -import { ROOT_HOLON_CARD, type HolonCard, HolonState, type HolonStateValue } from "./types.js"; +import { + ROOT_HOLON_CARD, + type HolonCard, + HolonState, + type HolonStateValue, +} from "./types.js"; import { getTaskManager, type CreateTaskOptions } from "../tasks/manager.js"; import { getLinearClient } from "../tools/linear-client.js"; import type { LLMUsage } from "../tools/types.js"; @@ -177,7 +182,9 @@ Output structured analysis with subtasks.`; // The actual analysis quality should be validated by the reviewer holon const success = adkResult.response.length > 100; - console.log(`[root] ADK analysis complete: ${adkResult.response.length} chars, ${adkResult.events.length} events`); + console.log( + `[root] ADK analysis complete: ${adkResult.response.length} chars, ${adkResult.events.length} events`, + ); // Update task state based on result if (success) { @@ -235,7 +242,8 @@ Output structured analysis with subtasks.`; error: errorMessage, }; } finally { - this.state = this.activeTasks.size > 0 ? HolonState.BUSY : HolonState.IDLE; + this.state = + this.activeTasks.size > 0 ? HolonState.BUSY : HolonState.IDLE; } } @@ -355,7 +363,8 @@ Output structured analysis with subtasks.`; const syncResult: LinearSyncResult = { task_id: task.id, linear_issue_id: linearRef.issue_id, - sync_status: errors.length === 0 ? SyncStatus.SUCCESS : SyncStatus.PARTIAL, + sync_status: + errors.length === 0 ? SyncStatus.SUCCESS : SyncStatus.PARTIAL, parent_issue_updated: true, sub_issues_created: [], labels_applied: ["ai-analyzed"], diff --git a/impl/mvp/src/holons/types.ts b/impl/mvp/src/holons/types.ts index 6359987..ef9d20f 100644 --- a/impl/mvp/src/holons/types.ts +++ b/impl/mvp/src/holons/types.ts @@ -372,7 +372,8 @@ export const CURSOR_REVIEWER_HOLON_CARD: HolonCard = { type: HolonType.LEAF, identity: { name: "I2P Cursor Reviewer", - description: "Reviews PRs created by Cursor agent against acceptance criteria", + description: + "Reviews PRs created by Cursor agent against acceptance criteria", parent: "holon:i2p:root", }, capabilities: { diff --git a/impl/mvp/src/tools/github-client.ts b/impl/mvp/src/tools/github-client.ts index 5f0a597..762e060 100644 --- a/impl/mvp/src/tools/github-client.ts +++ b/impl/mvp/src/tools/github-client.ts @@ -72,9 +72,7 @@ export class GitHubClient { */ static parsePRUrl(url: string): ParsedPRUrl | null { // Match: https://github.com/owner/repo/pull/123 - const match = url.match( - /github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/, - ); + const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/); if (!match) return null; return { @@ -123,7 +121,10 @@ export class GitHubClient { // Handle diff responses (text) const contentType = response.headers.get("content-type"); - if (contentType?.includes("text/plain") || headers?.Accept?.includes("diff")) { + if ( + contentType?.includes("text/plain") || + headers?.Accept?.includes("diff") + ) { return (await response.text()) as T; } @@ -133,7 +134,11 @@ export class GitHubClient { /** * Fetch PR details */ - async fetchPR(owner: string, repo: string, number: number): Promise { + async fetchPR( + owner: string, + repo: string, + number: number, + ): Promise { return this.request( "GET", `/repos/${owner}/${repo}/pulls/${number}`, @@ -143,7 +148,11 @@ export class GitHubClient { /** * Fetch PR diff */ - async fetchPRDiff(owner: string, repo: string, number: number): Promise { + async fetchPRDiff( + owner: string, + repo: string, + number: number, + ): Promise { return this.request( "GET", `/repos/${owner}/${repo}/pulls/${number}`, @@ -220,7 +229,10 @@ ${comment} /** * Request changes on a PR (tags @cursor for automated pickup) */ - async requestChanges(url: string, feedback: string): Promise { + async requestChanges( + url: string, + feedback: string, + ): Promise { const parsed = GitHubClient.parsePRUrl(url); if (!parsed) { throw new HCAError( diff --git a/impl/mvp/src/tools/linear-client.ts b/impl/mvp/src/tools/linear-client.ts index 5479ddc..822eb97 100644 --- a/impl/mvp/src/tools/linear-client.ts +++ b/impl/mvp/src/tools/linear-client.ts @@ -650,7 +650,9 @@ export class LinearClient { /** * Fetch issue with attachments (includes PR links) */ - async fetchIssueWithAttachments(id: string): Promise { + async fetchIssueWithAttachments( + id: string, + ): Promise { const data = await this.query<{ issue: LinearIssueWithAttachments | null }>( FETCH_ISSUE_WITH_ATTACHMENTS_QUERY, { id }, @@ -681,11 +683,14 @@ export class LinearClient { /** * Find a team member by name */ - async findTeamMemberByName(teamId: string, name: string): Promise { + async findTeamMemberByName( + teamId: string, + name: string, + ): Promise { const members = await this.fetchTeamMembers(teamId); - return members.find( - (m) => m.name.toLowerCase() === name.toLowerCase(), - ) ?? null; + return ( + members.find((m) => m.name.toLowerCase() === name.toLowerCase()) ?? null + ); } /** @@ -697,9 +702,13 @@ export class LinearClient { }>(SEARCH_USERS_QUERY, { filter: { name: { containsIgnoreCase: name } }, }); - return data.users.nodes.find( - (u) => u.name.toLowerCase() === name.toLowerCase(), - ) ?? data.users.nodes[0] ?? null; + return ( + data.users.nodes.find( + (u) => u.name.toLowerCase() === name.toLowerCase(), + ) ?? + data.users.nodes[0] ?? + null + ); } /** @@ -719,7 +728,9 @@ export class LinearClient { return false; } - console.log(`[linear] Assigning ${issue.identifier} to Cursor (${cursorUser.id})`); + console.log( + `[linear] Assigning ${issue.identifier} to Cursor (${cursorUser.id})`, + ); return this.assignIssue(issue.id, cursorUser.id); } } diff --git a/impl/mvp/src/tools/linear-watcher.ts b/impl/mvp/src/tools/linear-watcher.ts index 72f37e7..0f87738 100644 --- a/impl/mvp/src/tools/linear-watcher.ts +++ b/impl/mvp/src/tools/linear-watcher.ts @@ -230,7 +230,9 @@ export class LinearWatcher { return; // No subtasks being tracked } - console.log(`[linear-watcher] Running Cursor review loop (${trackedCount} tracked subtasks)`); + console.log( + `[linear-watcher] Running Cursor review loop (${trackedCount} tracked subtasks)`, + ); const results = await reviewer.runReviewLoop(); if (results.length > 0) { diff --git a/impl/mvp/src/tools/opencode-client.ts b/impl/mvp/src/tools/opencode-client.ts index bc4a3ca..002ba2a 100644 --- a/impl/mvp/src/tools/opencode-client.ts +++ b/impl/mvp/src/tools/opencode-client.ts @@ -12,11 +12,17 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2"; // ============================================================================ function getOpenCodeUrl(): string { - return process.env["HCA_OPENCODE_URL"] ?? process.env["OPENCODE_URL"] ?? "https://oc1.dev.arda.xyz"; + return ( + process.env["HCA_OPENCODE_URL"] ?? + process.env["OPENCODE_URL"] ?? + "https://oc1.dev.arda.xyz" + ); } function getDefaultModel(): string { - return process.env["HCA_OPENCODE_MODEL"] ?? "openrouter/anthropic/claude-opus-4.5"; + return ( + process.env["HCA_OPENCODE_MODEL"] ?? "openrouter/anthropic/claude-opus-4.5" + ); } // ============================================================================ @@ -153,7 +159,8 @@ export async function reason( const payload = event.payload; // Filter for reasoning parts from our session if (payload.type === "message.part.updated") { - const part = "properties" in payload ? payload.properties?.part : undefined; + const part = + "properties" in payload ? payload.properties?.part : undefined; // sessionID is on the part itself if ( part && diff --git a/impl/mvp/src/tools/opencode-pool.ts b/impl/mvp/src/tools/opencode-pool.ts index 9f4b313..d7a38ed 100644 --- a/impl/mvp/src/tools/opencode-pool.ts +++ b/impl/mvp/src/tools/opencode-pool.ts @@ -76,11 +76,15 @@ const DEFAULT_POOL_CONFIG: PoolConfig = { const DEFAULT_INSTANCE: InstanceConfig = { name: "local", - url: process.env["HCA_OPENCODE_URL"] ?? process.env["OPENCODE_URL"] ?? "http://localhost:4096", + url: + process.env["HCA_OPENCODE_URL"] ?? + process.env["OPENCODE_URL"] ?? + "http://localhost:4096", enabled: true, priority: 10, tags: ["local", "default"], - default_model: process.env["HCA_OPENCODE_MODEL"] ?? "openrouter/anthropic/claude-opus-4.5", + default_model: + process.env["HCA_OPENCODE_MODEL"] ?? "openrouter/anthropic/claude-opus-4.5", supported_models: ["*"], max_concurrent: 5, health_timeout: 5000, @@ -133,14 +137,19 @@ export class OpenCodePool { const content = readFileSync(configPath, "utf-8"); const parsed = parseYaml(content) as OpenCodePoolConfig; console.log( - `[opencode-pool] Loaded config from ${configPath}: ${parsed.instances?.length ?? 0} instances` + `[opencode-pool] Loaded config from ${configPath}: ${parsed.instances?.length ?? 0} instances`, ); return { pool: { ...DEFAULT_POOL_CONFIG, ...parsed.pool }, - instances: parsed.instances?.filter((i) => i.enabled) ?? [DEFAULT_INSTANCE], + instances: parsed.instances?.filter((i) => i.enabled) ?? [ + DEFAULT_INSTANCE, + ], }; } catch (error) { - console.warn(`[opencode-pool] Failed to load config from ${configPath}:`, error); + console.warn( + `[opencode-pool] Failed to load config from ${configPath}:`, + error, + ); } } @@ -203,7 +212,9 @@ export class OpenCodePool { lastHealthCheck: 0, activeSessions: 0, }); - console.log(`[opencode-pool] Added instance: ${config.name} (${config.url})`); + console.log( + `[opencode-pool] Added instance: ${config.name} (${config.url})`, + ); } // ============================================================================ @@ -217,36 +228,41 @@ export class OpenCodePool { // Periodic health checks this.healthCheckTimer = setInterval( () => this.runHealthChecks(), - this.config.pool.health_check_interval * 1000 + this.config.pool.health_check_interval * 1000, ); } private async runHealthChecks(): Promise { - const checks = Array.from(this.instances.entries()).map(async ([_name, state]) => { - try { - const controller = new AbortController(); - const timeout = setTimeout( - () => controller.abort(), - state.config.health_timeout - ); - - const health = await state.client.global.health(); - clearTimeout(timeout); + const checks = Array.from(this.instances.entries()).map( + async ([_name, state]) => { + try { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + state.config.health_timeout, + ); - state.healthy = health.data?.healthy ?? false; - state.version = health.data?.version; - state.error = undefined; - state.lastHealthCheck = Date.now(); - } catch (error) { - state.healthy = false; - state.error = error instanceof Error ? error.message : "Unknown error"; - state.lastHealthCheck = Date.now(); - } - }); + const health = await state.client.global.health(); + clearTimeout(timeout); + + state.healthy = health.data?.healthy ?? false; + state.version = health.data?.version; + state.error = undefined; + state.lastHealthCheck = Date.now(); + } catch (error) { + state.healthy = false; + state.error = + error instanceof Error ? error.message : "Unknown error"; + state.lastHealthCheck = Date.now(); + } + }, + ); await Promise.all(checks); - const healthyCount = Array.from(this.instances.values()).filter((i) => i.healthy).length; + const healthyCount = Array.from(this.instances.values()).filter( + (i) => i.healthy, + ).length; if (healthyCount === 0) { console.warn("[opencode-pool] WARNING: No healthy instances available!"); } @@ -270,7 +286,8 @@ export class OpenCodePool { if (state.activeSessions >= state.config.max_concurrent) return false; if (options?.excludeInstance === state.config.name) return false; if (options?.tags?.length) { - if (!options.tags.some((t) => state.config.tags.includes(t))) return false; + if (!options.tags.some((t) => state.config.tags.includes(t))) + return false; } return true; }); @@ -282,7 +299,7 @@ export class OpenCodePool { // Model-based filtering if (options?.model && this.config.pool.strategy === "model-match") { const modelCandidates = candidates.filter((state) => - this.instanceSupportsModel(state.config, options.model!) + this.instanceSupportsModel(state.config, options.model!), ); if (modelCandidates.length > 0) { candidates = modelCandidates; @@ -332,7 +349,10 @@ export class OpenCodePool { return candidates[0]; } - private instanceSupportsModel(config: InstanceConfig, model: string): boolean { + private instanceSupportsModel( + config: InstanceConfig, + model: string, + ): boolean { return config.supported_models.some((pattern) => { if (pattern === "*") return true; if (pattern.endsWith("/*")) { @@ -350,10 +370,7 @@ export class OpenCodePool { /** * Acquire an instance for a reasoning session */ - async acquire(options?: { - model?: string; - tags?: string[]; - }): Promise<{ + async acquire(options?: { model?: string; tags?: string[] }): Promise<{ instance: InstanceState; release: () => void; } | null> { @@ -376,17 +393,23 @@ export class OpenCodePool { * Execute a reasoning task with automatic retry and failover */ async executeWithRetry( - task: (client: ReturnType, instanceName: string) => Promise, + task: ( + client: ReturnType, + instanceName: string, + ) => Promise, options?: { model?: string; tags?: string[]; - } + }, ): Promise { let lastError: Error | null = null; let excludeInstance: string | undefined; for (let attempt = 0; attempt <= this.config.pool.max_retries; attempt++) { - const acquired = await this.acquire({ ...options, excludeInstance } as any); + const acquired = await this.acquire({ + ...options, + excludeInstance, + } as any); if (!acquired) { if (lastError) throw lastError; @@ -404,9 +427,12 @@ export class OpenCodePool { lastError = error instanceof Error ? error : new Error(String(error)); excludeInstance = instance.config.name; - if (this.config.pool.retry_on_failure && attempt < this.config.pool.max_retries) { + if ( + this.config.pool.retry_on_failure && + attempt < this.config.pool.max_retries + ) { console.warn( - `[opencode-pool] Task failed on ${instance.config.name}, retrying on another instance...` + `[opencode-pool] Task failed on ${instance.config.name}, retrying on another instance...`, ); // Mark instance as potentially unhealthy instance.healthy = false; @@ -436,7 +462,10 @@ export class OpenCodePool { return { totalInstances: this.instances.size, healthyInstances: instanceStats.filter((i) => i.healthy).length, - totalActiveSessions: instanceStats.reduce((sum, i) => sum + i.activeSessions, 0), + totalActiveSessions: instanceStats.reduce( + (sum, i) => sum + i.activeSessions, + 0, + ), instanceStats, }; } @@ -501,7 +530,7 @@ export async function reasonWithPool( model?: string; tags?: string[]; sessionTitle?: string; - } + }, ): Promise<{ result: string; cost: number; @@ -531,7 +560,8 @@ export async function reasonWithPool( } // Create session - const sessionTitle = options?.sessionTitle ?? `hca-pool-${Date.now().toString(36)}`; + const sessionTitle = + options?.sessionTitle ?? `hca-pool-${Date.now().toString(36)}`; const session = await client.session.create({ title: sessionTitle }); const sessionId = session.data?.id; @@ -540,7 +570,9 @@ export async function reasonWithPool( } // Execute prompt - console.log(`[opencode-pool] Executing on ${instanceName}: ${providerID}/${modelID}`); + console.log( + `[opencode-pool] Executing on ${instanceName}: ${providerID}/${modelID}`, + ); const result = await client.session.prompt({ sessionID: sessionId, From 241bac75dd643a0734fd9bb1833a0227b32a7f93 Mon Sep 17 00:00:00 2001 From: aWN4Y25pa2EK <19519604+aWN4Y25pa2EK@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:41:25 +0000 Subject: [PATCH 3/4] feat(ci): add GitHub Actions workflow for runtime container builds - Add docker-runtimes.yml workflow for automated multi-platform builds - Build base image first, then all runtime images in parallel - Support for linux/amd64 and linux/arm64 platforms - Add BASE_IMAGE build arg to all runtime Dockerfiles for CI compatibility - Trigger on push/PR to master when runtime files change - Generate build summaries with pull/run commands Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-runtimes.yml | 220 ++++++++++++++++++ impl/mvp/docker/runtimes/generic/Dockerfile | 3 +- .../docker/runtimes/infrastructure/Dockerfile | 3 +- impl/mvp/docker/runtimes/python/Dockerfile | 3 +- impl/mvp/docker/runtimes/rust/Dockerfile | 3 +- .../mvp/docker/runtimes/typescript/Dockerfile | 3 +- 6 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/docker-runtimes.yml diff --git a/.github/workflows/docker-runtimes.yml b/.github/workflows/docker-runtimes.yml new file mode 100644 index 0000000..df8adab --- /dev/null +++ b/.github/workflows/docker-runtimes.yml @@ -0,0 +1,220 @@ +# GitHub Actions Workflow: Build and Push HCA Runtime Docker Images +# +# This workflow builds multi-platform Docker images for HCA runtime containers +# and pushes them to GitHub Container Registry (ghcr.io). +# +# Build Order: +# 1. Base image (hca-runtime-base) - all others depend on this +# 2. Language-specific images in parallel (rust, typescript, python, generic, infrastructure) +# +# Triggers: +# - Push to master branch (when Dockerfile or config changes) +# - Pull requests (builds but doesn't push) +# - Release publications (tags with version) +# - Manual dispatch +# +# Image Tags: +# - latest (master branch) +# - sha- (all builds) +# - v1.0.0, v1.0, v1 (releases) + +name: Build HCA Runtime Containers + +on: + push: + branches: [master] + paths: + - 'impl/mvp/docker/runtimes/**' + - '.github/workflows/docker-runtimes.yml' + pull_request: + branches: [master] + paths: + - 'impl/mvp/docker/runtimes/**' + - '.github/workflows/docker-runtimes.yml' + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ghcr.io/ardaglobal/hca-runtime + +jobs: + # ========================================================================== + # Build Base Image First (all others depend on this) + # ========================================================================== + build-base: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for base + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}-base + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push base image + uses: docker/build-push-action@v5 + id: build + with: + context: ./impl/mvp/docker/runtimes/base + file: ./impl/mvp/docker/runtimes/base/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=base + cache-to: type=gha,mode=max,scope=base + + # ========================================================================== + # Build Runtime Images (depend on base) + # ========================================================================== + build-runtimes: + needs: build-base + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: + runtime: + - name: rust + port: 4100 + - name: typescript + port: 4101 + - name: python + port: 4102 + - name: generic + port: 4103 + - name: infrastructure + port: 4104 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for ${{ matrix.runtime.name }} + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}-${{ matrix.runtime.name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push ${{ matrix.runtime.name }} image + uses: docker/build-push-action@v5 + id: build + with: + context: ./impl/mvp/docker/runtimes/${{ matrix.runtime.name }} + file: ./impl/mvp/docker/runtimes/${{ matrix.runtime.name }}/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.runtime.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.runtime.name }} + build-args: | + BASE_IMAGE=${{ env.IMAGE_PREFIX }}-base:latest + + - name: Generate build summary + if: success() + run: | + echo "## 🐳 ${{ matrix.runtime.name }} Runtime Build" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Image | \`${{ env.IMAGE_PREFIX }}-${{ matrix.runtime.name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Port | ${{ matrix.runtime.port }} |" >> $GITHUB_STEP_SUMMARY + echo "| Platforms | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY + echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Pull & Run" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "docker pull ${{ env.IMAGE_PREFIX }}-${{ matrix.runtime.name }}:latest" >> $GITHUB_STEP_SUMMARY + echo "docker run -d -p ${{ matrix.runtime.port }}:4096 \\" >> $GITHUB_STEP_SUMMARY + echo " -e OPENROUTER_API_KEY=\$OPENROUTER_API_KEY \\" >> $GITHUB_STEP_SUMMARY + echo " ${{ env.IMAGE_PREFIX }}-${{ matrix.runtime.name }}:latest" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + # ========================================================================== + # Summary Job + # ========================================================================== + summary: + needs: [build-base, build-runtimes] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate workflow summary + run: | + echo "# πŸš€ HCA Runtime Containers Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Images Built" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | Image | Port |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|------|" >> $GITHUB_STEP_SUMMARY + echo "| Base | \`${{ env.IMAGE_PREFIX }}-base\` | - |" >> $GITHUB_STEP_SUMMARY + echo "| Rust | \`${{ env.IMAGE_PREFIX }}-rust\` | 4100 |" >> $GITHUB_STEP_SUMMARY + echo "| TypeScript | \`${{ env.IMAGE_PREFIX }}-typescript\` | 4101 |" >> $GITHUB_STEP_SUMMARY + echo "| Python | \`${{ env.IMAGE_PREFIX }}-python\` | 4102 |" >> $GITHUB_STEP_SUMMARY + echo "| Generic | \`${{ env.IMAGE_PREFIX }}-generic\` | 4103 |" >> $GITHUB_STEP_SUMMARY + echo "| Infrastructure | \`${{ env.IMAGE_PREFIX }}-infrastructure\` | 4104 |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Quick Start" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "# Pull all images" >> $GITHUB_STEP_SUMMARY + echo "for rt in base rust typescript python generic infrastructure; do" >> $GITHUB_STEP_SUMMARY + echo " docker pull ${{ env.IMAGE_PREFIX }}-\$rt:latest" >> $GITHUB_STEP_SUMMARY + echo "done" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + env: + IMAGE_PREFIX: ghcr.io/ardaglobal/hca-runtime diff --git a/impl/mvp/docker/runtimes/generic/Dockerfile b/impl/mvp/docker/runtimes/generic/Dockerfile index f532697..097f923 100644 --- a/impl/mvp/docker/runtimes/generic/Dockerfile +++ b/impl/mvp/docker/runtimes/generic/Dockerfile @@ -13,7 +13,8 @@ # # ============================================================================ -FROM hca-runtime-base:latest +ARG BASE_IMAGE=hca-runtime-base:latest +FROM ${BASE_IMAGE} USER root diff --git a/impl/mvp/docker/runtimes/infrastructure/Dockerfile b/impl/mvp/docker/runtimes/infrastructure/Dockerfile index ce15a14..3128eb2 100644 --- a/impl/mvp/docker/runtimes/infrastructure/Dockerfile +++ b/impl/mvp/docker/runtimes/infrastructure/Dockerfile @@ -13,7 +13,8 @@ # # ============================================================================ -FROM hca-runtime-base:latest +ARG BASE_IMAGE=hca-runtime-base:latest +FROM ${BASE_IMAGE} USER root diff --git a/impl/mvp/docker/runtimes/python/Dockerfile b/impl/mvp/docker/runtimes/python/Dockerfile index c1d0249..1a9f63f 100644 --- a/impl/mvp/docker/runtimes/python/Dockerfile +++ b/impl/mvp/docker/runtimes/python/Dockerfile @@ -13,7 +13,8 @@ # # ============================================================================ -FROM hca-runtime-base:latest +ARG BASE_IMAGE=hca-runtime-base:latest +FROM ${BASE_IMAGE} USER root diff --git a/impl/mvp/docker/runtimes/rust/Dockerfile b/impl/mvp/docker/runtimes/rust/Dockerfile index 3512bd1..e3ebcb6 100644 --- a/impl/mvp/docker/runtimes/rust/Dockerfile +++ b/impl/mvp/docker/runtimes/rust/Dockerfile @@ -13,7 +13,8 @@ # # ============================================================================ -FROM hca-runtime-base:latest +ARG BASE_IMAGE=hca-runtime-base:latest +FROM ${BASE_IMAGE} USER root diff --git a/impl/mvp/docker/runtimes/typescript/Dockerfile b/impl/mvp/docker/runtimes/typescript/Dockerfile index 47b4ca2..0bd7102 100644 --- a/impl/mvp/docker/runtimes/typescript/Dockerfile +++ b/impl/mvp/docker/runtimes/typescript/Dockerfile @@ -13,7 +13,8 @@ # # ============================================================================ -FROM hca-runtime-base:latest +ARG BASE_IMAGE=hca-runtime-base:latest +FROM ${BASE_IMAGE} USER root From 2a4ccf770c6bcc26348b7b4c8332aa7252eb74e3 Mon Sep 17 00:00:00 2001 From: aWN4Y25pa2EK <19519604+aWN4Y25pa2EK@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:51:43 +0000 Subject: [PATCH 4/4] fix(ci): resolve race condition in runtime container builds - Use digest-based image references (immutable, immediately available) - Add registry availability check with retry loop (30 attempts, 10s each) - Skip runtime builds on PRs (base isn't pushed, so runtimes can't build) - Add separate PR validation summary - Pass base image digest to runtime builds via BASE_IMAGE arg Fixes issue where runtime builds failed because base image wasn't propagated in registry yet after push. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-runtimes.yml | 73 ++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker-runtimes.yml b/.github/workflows/docker-runtimes.yml index df8adab..b5a1f4f 100644 --- a/.github/workflows/docker-runtimes.yml +++ b/.github/workflows/docker-runtimes.yml @@ -3,20 +3,15 @@ # This workflow builds multi-platform Docker images for HCA runtime containers # and pushes them to GitHub Container Registry (ghcr.io). # -# Build Order: -# 1. Base image (hca-runtime-base) - all others depend on this -# 2. Language-specific images in parallel (rust, typescript, python, generic, infrastructure) +# Build Strategy: +# - Push/Release: Build base β†’ wait for registry β†’ build runtimes with digest reference +# - Pull Request: Build base only (validation) - runtimes skipped since base isn't pushed # # Triggers: # - Push to master branch (when Dockerfile or config changes) -# - Pull requests (builds but doesn't push) +# - Pull requests (base build validation only) # - Release publications (tags with version) # - Manual dispatch -# -# Image Tags: -# - latest (master branch) -# - sha- (all builds) -# - v1.0.0, v1.0, v1 (releases) name: Build HCA Runtime Containers @@ -50,6 +45,7 @@ jobs: packages: write outputs: digest: ${{ steps.build.outputs.digest }} + image-ref: ${{ steps.image-ref.outputs.ref }} steps: - name: Checkout repository @@ -94,11 +90,23 @@ jobs: cache-from: type=gha,scope=base cache-to: type=gha,mode=max,scope=base + - name: Set image reference output + id: image-ref + run: | + if [ "${{ github.event_name }}" != "pull_request" ]; then + # Use digest for immutable reference (immediately available after push) + echo "ref=${{ env.IMAGE_PREFIX }}-base@${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT + else + echo "ref=" >> $GITHUB_OUTPUT + fi + # ========================================================================== - # Build Runtime Images (depend on base) + # Build Runtime Images (depend on base, skip on PRs) # ========================================================================== build-runtimes: needs: build-base + # Skip runtime builds on PRs since base image isn't pushed to registry + if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read @@ -136,6 +144,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Wait for base image availability + run: | + BASE_REF="${{ needs.build-base.outputs.image-ref }}" + echo "Waiting for base image: $BASE_REF" + + for i in {1..30}; do + if docker pull "$BASE_REF" 2>/dev/null; then + echo "βœ… Base image available after $i attempt(s)" + exit 0 + fi + echo "⏳ Attempt $i/30: Base image not yet available, waiting 10s..." + sleep 10 + done + + echo "❌ Base image not available after 5 minutes" + exit 1 + - name: Extract metadata for ${{ matrix.runtime.name }} id: meta uses: docker/metadata-action@v5 @@ -156,13 +181,13 @@ jobs: context: ./impl/mvp/docker/runtimes/${{ matrix.runtime.name }} file: ./impl/mvp/docker/runtimes/${{ matrix.runtime.name }}/Dockerfile platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=${{ matrix.runtime.name }} cache-to: type=gha,mode=max,scope=${{ matrix.runtime.name }} build-args: | - BASE_IMAGE=${{ env.IMAGE_PREFIX }}-base:latest + BASE_IMAGE=${{ needs.build-base.outputs.image-ref }} - name: Generate build summary if: success() @@ -172,6 +197,7 @@ jobs: echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Image | \`${{ env.IMAGE_PREFIX }}-${{ matrix.runtime.name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Base | \`${{ needs.build-base.outputs.image-ref }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Port | ${{ matrix.runtime.port }} |" >> $GITHUB_STEP_SUMMARY echo "| Platforms | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY @@ -190,7 +216,7 @@ jobs: summary: needs: [build-base, build-runtimes] runs-on: ubuntu-latest - if: always() + if: always() && github.event_name != 'pull_request' steps: - name: Generate workflow summary @@ -218,3 +244,24 @@ jobs: echo "\`\`\`" >> $GITHUB_STEP_SUMMARY env: IMAGE_PREFIX: ghcr.io/ardaglobal/hca-runtime + + # ========================================================================== + # PR Validation Summary + # ========================================================================== + pr-summary: + needs: [build-base] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Generate PR summary + run: | + echo "# πŸ” PR Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Base Image" >> $GITHUB_STEP_SUMMARY + echo "βœ… Base image builds successfully" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Runtime Images" >> $GITHUB_STEP_SUMMARY + echo "⏭️ Skipped (only built on push to master)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Runtime images will be built and pushed when this PR is merged." >> $GITHUB_STEP_SUMMARY