diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index f859419..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,21 +0,0 @@ -# Portable Coder — Agent Map (AGENTS.md) - -This repository is run with harness-first planning. Treat `docs/` as the system of record. - -## Read order (always) -1. This file -2. HARNESS_CHECKLIST.md -3. docs/SECURITY.md -4. docs/ARCHITECTURE.md -5. docs/PLANS.md -6. Active ExecPlan in docs/exec-plans/active/ - -## Non-negotiable rules -- No implementation work starts without an ExecPlan in `docs/exec-plans/active/`. -- Keep plan artifacts updated during execution, not after. -- Required control docs include `owner:` and `last_verified:` metadata. - -## Work style -- Keep PRs small and reviewable. -- Prefer deterministic, cross-platform tooling. -- If blocked, improve docs/harness/tools instead of guessing. diff --git a/CODEX_EXECUTION_GUIDE.md b/CODEX_EXECUTION_GUIDE.md deleted file mode 100644 index e032acc..0000000 --- a/CODEX_EXECUTION_GUIDE.md +++ /dev/null @@ -1,52 +0,0 @@ -# Codex Execution Guide (Harness-First Workflow) - -Date: 2026-02-18 - -This repository uses harness-first execution. Repo docs + active plan are the source of truth. - -## Operator Role -You prioritize work, approve plan scope, and validate outcomes. - -## Standard Execution Loop -1. Confirm an ExecPlan exists in `docs/exec-plans/active/`. -2. Execute that plan end-to-end. -3. Update plan live while executing: - - Progress checkboxes with dates - - Decision log entries - - Surprises/discoveries notes -4. Validate acceptance criteria. -5. Close out and move plan to `docs/exec-plans/completed/`. - -## Prompt Template -```text -Read AGENTS.md and HARNESS_CHECKLIST.md first. -Then read docs/PLANS.md. - -Execute the ExecPlan at: -docs/exec-plans/active/.md - -Rules: -- Follow plan steps and required validations. -- Keep the plan updated as you work. -- Keep changes small and reviewable. -- If blocked, improve docs/harness/tools instead of guessing. - -Output: -- Summary of changes -- Validation results -- Remaining risks and follow-up tasks -``` - -## Build-Required Intake -Idea intake files live in: -- `docs/exec-plans/build_required/` - -Workflow: -1. Convert intake notes into full ExecPlans. -2. Move new plan into `docs/exec-plans/active/`. -3. Move processed intake notes into `docs/exec-plans/completed/` archive. - -## Guardrails -- No implementation work without an active plan. -- Keep Windows compatibility from day 0. -- Prefer cross-platform scripts and deterministic runtime setup. diff --git a/HARNESS_CHECKLIST.md b/HARNESS_CHECKLIST.md deleted file mode 100644 index f76436a..0000000 --- a/HARNESS_CHECKLIST.md +++ /dev/null @@ -1,45 +0,0 @@ -# Harness Engineering Checklist - -## 1. Repository as System of Record -- [ ] `AGENTS.md` is concise and links to deeper docs -- [ ] Core docs exist: `ARCHITECTURE.md`, `DESIGN.md`, `QUALITY.md`, `PLANS.md`, `SECURITY.md`, `RUNBOOKS.md` -- [ ] Plan directories exist: `exec-plans/active`, `exec-plans/build_required`, `exec-plans/completed` -- [ ] Decisions are captured in repo artifacts, not only in chat -- [ ] Execution plans are first-class, versioned artifacts - -## 2. Agent Legibility -- [ ] A fresh agent session can understand the system from repo docs alone -- [ ] Prompts start from `AGENTS.md`, checklist, and active plan -- [ ] Hidden tribal knowledge is promoted into docs or checks - -## 3. Architecture Enforcement -- [ ] Layer boundaries are documented and enforced -- [ ] Security and portability constraints are explicit -- [ ] Runtime dependencies are deterministic and reproducible - -## 4. Documentation Hygiene -- [ ] Key docs have owner + `last_verified` -- [ ] Docs are cross-linked through `docs/DOCS_INDEX.md` -- [ ] Stale docs are regularly reviewed and pruned - -## 5. Quality and Recovery -- [ ] Acceptance criteria are behavior-first and testable -- [ ] Idempotence and rollback paths exist for each execution plan -- [ ] Risks, open questions, and decisions are tracked separately - -## 6. Agent Workflow -- [ ] Every material task starts from an active ExecPlan -- [ ] Progress, decisions, and surprises are logged during execution -- [ ] Plan is moved to completed after closeout - -## 7. Feedback Loops -- [ ] Validation commands are documented and repeatable -- [ ] Operator can quickly see state, risk, and next actions - -## 8. Merge Philosophy -- [ ] Small changes and short-lived branches are preferred -- [ ] Corrections are cheap; waiting is expensive - -## 9. Human Leverage -- [ ] Human focus is on intent, constraints, and acceptance -- [ ] Repeated review feedback is promoted into tooling/docs diff --git a/README.md b/README.md index 4c1fd6e..a760d27 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,117 @@ -# Portable Coder +# Portable Claude Code -Portable multi-provider coding CLI launcher with harness-first planning. +A portable Claude Code launcher you can copy to any Windows computer or flash drive and run without a system-wide install. -## Current Status -- Planning control-plane is established (`AGENTS.md`, `HARNESS_CHECKLIST.md`, `docs/*`). -- Active plan: `docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md`. -- MVP launcher exists: `scripts/pcoder` / `scripts/pcoder.cmd`. -- MVP platform scope is currently Windows-first. -- Windows target hosts: Windows 11, Server 2016, Server 2022, Server 2025. -- MVP tool scope is Codex + Claude only. -- WSL is optional when present, but not required. -- Primary Linux backend for Windows MVP is bundled QEMU VM. -- VM policy is try hardware acceleration first, then auto fallback to portable software mode. +## What it does + +- Launches [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI with your local filesystem accessible +- Stores OAuth credentials portably inside the repo's `state/` folder (no registry or system config pollution) +- On Windows: runs Claude Code inside a bundled QEMU Linux VM (no WSL required) +- Supports API key mode if you prefer to skip OAuth + +## Requirements + +- **Node.js** — either bundled in `runtime/node/` or installed on the host +- **Claude Code** — either installed globally (`npm install -g @anthropic-ai/claude-code`) or bundled in `runtime/` +- **Windows VM mode only**: QEMU runtime (download with `pcoder runtime bootstrap`) ## Quick Start -1. Bootstrap runtime payload (Windows): `scripts/runtime/windows/bootstrap-runtime.cmd` - - Optional launcher path: `scripts/pcoder runtime bootstrap` - - Optional URL overrides: `PCODER_QEMU_INSTALLER_URL`, `PCODER_QEMU_SHA512_URL`, `PCODER_UBUNTU_IMAGE_URL` -2. Run onboarding once: `scripts/pcoder setup --init`. -3. Choose auth modes as needed: - - OAuth: `scripts/pcoder setup --codex-auth oauth --claude-auth oauth` - - API keys: `scripts/pcoder setup --codex-auth api --claude-auth api` -4. Inject required env vars only for API mode (`OPENAI_API_KEY` and/or `ANTHROPIC_AUTH_TOKEN`). -5. Run `scripts/pcoder doctor`. -6. Launch in VM mode (Windows): `scripts/pcoder run --mode linux-portable`. -7. Use `--no-sync-back` if you do not want VM changes copied back automatically. - -On Windows, `pcoder run` defaults to `--mode linux-portable`. - -Auth management: -- `scripts/pcoder auth status` -- `scripts/pcoder auth login codex` -- `scripts/pcoder auth login claude` -- `scripts/pcoder auth logout codex` -- `scripts/pcoder auth logout claude` - -Windows smoke test: -- `scripts/runtime/windows/smoke-check.cmd` - -## CI -- GitHub Actions workflow: `.github/workflows/ci.yml` -- Runner target: Blacksmith runner label `blacksmith-2vcpu-ubuntu-2404` - -## Claude PR Runner -- Workflow: `.github/workflows/claude-pr-runner.yml` -- Trigger: mention `@claude` in PR comments/reviews (or run manually via `workflow_dispatch`) -- Runner target: Blacksmith runner label `blacksmith-2vcpu-ubuntu-2404` -- Configure one auth secret: - - `ANTHROPIC_API_KEY` (API key mode), or - - `CLAUDE_CODE_OAUTH_TOKEN` (OAuth mode) - -Supported tool IDs: -- `codex` -- `claude` - -## Planning Workflow -Read in order: -1. `AGENTS.md` -2. `HARNESS_CHECKLIST.md` -3. `docs/PLANS.md` -4. active ExecPlan in `docs/exec-plans/active/` + +### First time + +```bat +REM 1. Initialize settings (creates state/settings.json) +scripts\pcoder setup --init + +REM 2. Log in with your Anthropic account (OAuth - credentials stay portable) +scripts\pcoder auth login + +REM 3. Check everything looks good +scripts\pcoder doctor +``` + +### Launch Claude Code + +```bat +REM Launch in current directory +scripts\pcoder + +REM Launch in a specific project +scripts\pcoder run --project C:\path\to\my-project + +REM Pass args directly to claude +scripts\pcoder -- --help +``` + +### API key mode (skip OAuth) + +```bat +REM Switch to API key mode +scripts\pcoder setup --claude-auth api + +REM Set your key before launching +set ANTHROPIC_API_KEY=sk-ant-... +scripts\pcoder +``` + +## Windows: VM vs Host-Native + +By default on Windows, Claude runs inside a bundled portable Linux VM (QEMU). This gives full Linux compatibility without requiring WSL or any host configuration. + +**VM mode (default on Windows)** +```bat +REM Bootstrap the VM runtime first (one-time download) +scripts\pcoder runtime bootstrap + +REM Then just run normally +scripts\pcoder +``` + +**Host-native mode** (if claude is installed directly on Windows) +```bat +scripts\pcoder setup --windows-mode host-native +scripts\pcoder +``` + +## Commands + +| Command | Description | +|---|---| +| `pcoder` | Launch Claude Code in current directory | +| `pcoder doctor` | Check environment health | +| `pcoder setup --init` | Initialize settings | +| `pcoder setup --show` | Show current settings | +| `pcoder setup --claude-auth ` | Change auth mode | +| `pcoder setup --windows-mode ` | Change Windows run mode | +| `pcoder auth status` | Show auth status | +| `pcoder auth login` | Log in via OAuth | +| `pcoder auth logout` | Log out | +| `pcoder runtime probe` | Show available runtimes | +| `pcoder runtime bootstrap` | Download Windows VM runtime (QEMU + Ubuntu) | +| `pcoder run [--project ] [--mode ] [-- ]` | Run with options | + +## File Layout + +``` +PortableCoder/ + scripts/ + pcoder.cmd <- Windows launcher (double-click or run from cmd) + pcoder <- Linux/macOS launcher + pcoder.cjs <- Main launcher logic (Node.js) + runtime/windows/ <- Windows VM helper scripts + runtime/ + node/ <- (optional) bundled Node.js + linux/ <- VM image and SSH key (after bootstrap) + qemu/ <- QEMU binaries (after bootstrap) + state/ + settings.json <- Your settings (auto-created) + auth/ <- Portable OAuth credentials (stays with the drive) +profiles/ + profiles.json <- Anthropic profile config +``` + +## Portability Notes + +- Copy the entire `PortableCoder/` folder to a flash drive or another machine and it works +- OAuth credentials are stored in `state/auth/` so they travel with the folder +- No registry writes, no system PATH changes, no admin rights required (VM mode may need Hyper-V or software TCG fallback) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 1403066..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Architecture - -High-level boundaries and portability strategy for Portable Coder. - -## Product Objective -A portable, machine-to-machine coder toolkit that runs multiple coding CLIs without requiring host-level system install. -Primary goal: run tools inside a portable Linux (Ubuntu-like) runtime whenever possible. - -## Platform Support -- MVP target platform: Windows 11 -- Additional target hosts: Windows Server 2016, 2022, 2025 -- Post-MVP target platforms: macOS, Linux -- Preferred execution environment (long-term): bundled Linux runtime -- WSL can be used when available, but it is not a requirement - -## Windows Backend Strategy (MVP) -- Primary backend: bundled QEMU VM running an Ubuntu-based Linux guest. -- Optional backend: host WSL runtime when available and explicitly selected. -- Non-goal: requiring WSL or preinstalled virtualization toolchains on host. -- Acceleration policy: try hardware acceleration first, auto fallback to portable software virtualization. - -## Core Layers -1. `runtime/` — bundled dependencies and Linux runtime assets (rootfs, helper binaries) -2. `apps/` — provider tool wrappers and adapter configs -3. `profiles/` — user/provider profiles, endpoints, and model routing templates -4. `state/` — local cache, logs, and session state -5. `scripts/` — launchers, bootstrap/update scripts, diagnostics -6. `docs/` — planning and governance artifacts - -## Boundary Rules -- Launcher scripts may read `profiles/` and `state/`, never hardcode secrets. -- Provider adapters should normalize env var contracts and CLI arguments. -- Secrets must be injected from host environment at runtime (no secret files in bundle). -- Onboarding preferences live in `state/settings.json`; OAuth cache is isolated under `state/auth/`. -- Runtime executor should not assume hypervisor/container tools are preinstalled. - -## Runtime Modes -- `linux-portable` (preferred): launch CLIs inside bundled QEMU Ubuntu guest. -- `linux-wsl` (optional): use host WSL runtime if present. -- `host-native` (dev fallback): launch locally available binaries directly from host shell. - -The launcher should support explicit mode selection and deterministic fallback rules. -No production path should require preinstalled WSL. - -## Host Requirements (Expected) -- Ability to execute bundled binaries from portable folder. -- Sufficient CPU and memory for VM runtime. -- Sufficient disk space for VM image and workspace data. -- Administrator rights may improve acceleration options on some hosts, but are not assumed for baseline portability. - -## Initial Provider Scope -- MVP: Codex and Claude Code only -- Deferred post-MVP: Kimi, GLM, MiniMax, and other provider adapters diff --git a/docs/DECISIONS_LOG.md b/docs/DECISIONS_LOG.md deleted file mode 100644 index f533107..0000000 --- a/docs/DECISIONS_LOG.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Decision Log - -Status meanings: -- Open: not decided -- Proposed: options documented -- Decided: selected with rationale -- Implemented: shipped and verified -- Deprecated: replaced - -| ID | Title | Status | Owner | Target Milestone | Evidence/Links | -|---|---|---|---|---|---| -| D-001 | Windows-first vs multi-OS simultaneous MVP | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-002 | MVP tool scope (Codex/Claude only vs wider provider set) | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-003 | Secret storage model for portable media | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-004 | Runtime distribution model (pre-bundled vs bootstrap-download) | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-005 | Update strategy (manual, scripted, auto-check) | Open | Mike | EP-002 | OPEN_QUESTIONS Q5 | -| D-006 | WSL policy for Windows hosts | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-007 | Linux base image strategy (Ubuntu LTS version and packaging format) | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-008 | Primary no-install Linux backend for Windows hosts | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-009 | Windows Server 2016 backend performance/support posture | Decided | Mike | EP-001 | EP-001 Decision Log (2026-02-18) | -| D-010 | Auth mode model (OAuth + API support with portable onboarding settings) | Decided | Mike | EP-001 | EP-001 implementation notes (2026-02-18) | - -### D-001: Windows-first vs multi-OS simultaneous MVP -- Status: Decided -- Problem: Scope for initial delivery was split between Windows-only and cross-platform MVP. -- Options: - - Windows-first MVP - - Simultaneous Windows + macOS + Linux MVP -- Recommendation: Windows-first MVP. -- Rationale: Reduces execution risk and allows faster validation of portability model before broad platform support. -- Acceptance Criteria: - - EP-001 scope and runbooks prioritize Windows flow. - - Non-Windows support is explicitly marked post-MVP. -- Consequences: - - Faster path to first usable release. - - Cross-platform parity work deferred to a follow-up EP. -- Links: `docs/OPEN_QUESTIONS.md` (Q1), `docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md` - -### D-002: MVP tool scope (Codex/Claude only vs wider provider set) -- Status: Decided -- Problem: Early plan included broader provider adapters that increased MVP scope. -- Options: - - Codex + Claude only - - Codex + Claude + additional provider adapters in MVP -- Recommendation: Codex + Claude only. -- Rationale: Narrowest path to deliver a stable portable MVP quickly. -- Acceptance Criteria: - - Adapter catalog and docs list only Codex and Claude for MVP. - - Other providers are explicitly deferred. -- Consequences: - - Faster MVP delivery. - - Additional provider adapters move to follow-up EP. -- Links: `docs/OPEN_QUESTIONS.md` (Q2), `scripts/adapters/catalog.json` - -### D-003: Secret storage model for portable media -- Status: Decided -- Problem: Storing secrets on removable media increases leakage risk. -- Options: - - Plain local env files - - Encrypted local secret store - - External injection only -- Recommendation: External injection only. -- Rationale: Avoids at-rest secrets on portable artifact. -- Acceptance Criteria: - - MVP docs and config require shell/session env injection. - - No default workflow writes secrets under repo paths. -- Consequences: - - Safer default posture. - - Slightly more manual launch process when secret-backed providers are used. -- Links: `docs/OPEN_QUESTIONS.md` (Q3), `profiles/profiles.json`, `profiles/defaults/README.md` - -### D-004: Runtime distribution model (pre-bundled vs bootstrap-download) -- Status: Decided -- Problem: User wants machine-to-machine portability from a copied folder/zip. -- Options: - - Bootstrap-download on first run - - Pre-bundled binaries in distribution artifact -- Recommendation: Pre-bundled distribution artifact. -- Rationale: Guarantees offline portability and reduces setup friction on arbitrary hosts. -- Acceptance Criteria: - - Release artifact can be copied to removable media and launched directly. - - No mandatory first-run download for core runtime/tooling. -- Consequences: - - Larger artifact size. - - Stronger release packaging discipline required. -- Links: `docs/OPEN_QUESTIONS.md` (Q4), `docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md` - -### D-006: WSL policy for Windows hosts -- Status: Decided -- Problem: Windows hosts may or may not have WSL installed. -- Options: - - Require WSL - - Disallow WSL entirely - - WSL optional but never required -- Recommendation: WSL optional but never required. -- Rationale: Preserves portability requirement while allowing opportunistic use of existing WSL installations. -- Acceptance Criteria: - - Portable runtime has a no-WSL-required execution path. - - Runtime probe reports WSL availability but does not treat missing WSL as blocker. -- Consequences: - - Need a bundled no-install Linux backend path for guaranteed portability. - - Additional runtime implementation complexity. -- Links: `docs/OPEN_QUESTIONS.md` (Q6), `scripts/pcoder.cjs` - -### D-008: Primary no-install Linux backend for Windows hosts -- Status: Decided -- Problem: MVP requires Linux backend portability across Windows 11 and Windows Server without requiring WSL. -- Options: - - Bundled QEMU VM backend - - Alternate backend that depends on host-installed components -- Recommendation: Bundled QEMU VM backend. -- Rationale: Best match for folder-level portability with no hard dependency on host WSL presence. -- Acceptance Criteria: - - Portable artifact includes VM runtime components and launch scripts. - - Linux backend launch path works even when WSL is absent. -- Consequences: - - Artifact size increases. - - Need explicit handling for slower software virtualization on some hosts. -- Links: `docs/OPEN_QUESTIONS.md` (Q8), `docs/RUNTIME_WINDOWS_VM_BACKEND.md` - -### D-007: Linux base image strategy (Ubuntu LTS version and packaging format) -- Status: Decided -- Problem: Need a stable Ubuntu baseline for VM packaging and compatibility. -- Options: - - Ubuntu 22.04 LTS - - Ubuntu 24.04 LTS -- Recommendation: Ubuntu 24.04 LTS. -- Rationale: Selected as current baseline for MVP runtime image. -- Acceptance Criteria: - - Runtime manifest and backend docs reference Ubuntu 24.04 LTS. - - VM build/packaging assets align with chosen baseline. -- Consequences: - - Future image updates track Ubuntu 24.04 patch lifecycle. -- Links: `docs/OPEN_QUESTIONS.md` (Q7), `runtime/linux/vm-manifest.json`, `docs/RUNTIME_WINDOWS_VM_BACKEND.md` - -### D-009: Windows Server 2016 backend performance/support posture -- Status: Decided -- Problem: Some Server 2016 hosts may not provide hardware acceleration for virtualization. -- Options: - - Require acceleration and drop unsupported hosts - - Accept software virtualization fallback -- Recommendation: Accept software virtualization fallback. -- Rationale: Preserves no-install portability across target host set. -- Acceptance Criteria: - - VM launcher attempts hardware acceleration first. - - Launcher auto-fallbacks to portable software virtualization when needed. - - Server 2016 remains a supported target with documented performance caveat. -- Consequences: - - Potentially slower performance on some hosts. - - Better portability coverage for mixed Windows environments. -- Links: `docs/OPEN_QUESTIONS.md` (Q9), `scripts/runtime/windows/start-vm.ps1`, `docs/RUNTIME_WINDOWS_VM_BACKEND.md` - -### D-010: Auth mode model (OAuth + API support with portable onboarding settings) -- Status: Decided -- Problem: Portable runtime needs to support subscription OAuth users and API-key users without storing API secrets at rest. -- Options: - - API-only mode - - OAuth-only mode - - Dual-mode support with per-tool configuration -- Recommendation: Dual-mode support with per-tool persisted settings. -- Rationale: Meets both subscription and API-key workflows while keeping API key handling external-injection only. -- Acceptance Criteria: - - `pcoder setup` can persist auth mode per tool (`oauth|api`) and runtime defaults. - - `pcoder auth status|login|logout` is available for Codex and Claude. - - API mode keeps env-key injection requirements; OAuth mode persists session state to portable state paths. -- Consequences: - - Additional operator setup step (`pcoder setup --init`). - - OAuth cache lives on portable storage and must be treated as sensitive. -- Links: `scripts/pcoder.cjs`, `README.md`, `docs/SECURITY.md`, `docs/RUNBOOKS.md` - -## Decision Record Template -### D-###: -- Status: -- Problem: -- Options: -- Recommendation: -- Rationale: -- Acceptance Criteria: -- Consequences: -- Links: diff --git a/docs/DESIGN.md b/docs/DESIGN.md deleted file mode 100644 index e2ae20a..0000000 --- a/docs/DESIGN.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Design - -## UX Principles -- Single entrypoint command (`pcoder`) for all providers. -- Explicit profile selection (`pcoder --profile <name> <tool> ...`). -- Deterministic local paths so USB portability works. -- Linux-runtime-first execution with explicit fallback to host-native mode. -- WSL may be used if present but must not be required. - -## Configuration Model -- `profiles/profiles.json` for profile requirements and non-secret runtime options. -- `state/settings.json` for persisted onboarding settings (auth mode per tool, default runtime mode). -- Runtime secrets are injected from host environment variables per session. -- OAuth session/cache state is kept under `state/auth/` in the portable folder. -- No API key files are stored by default in MVP. - -## Command Surface (v0.1) -- `pcoder doctor` — verify runtime and provider readiness -- `pcoder list-tools` — show installed/available adapters -- `pcoder runtime probe` — detect available Linux runtime backends on host -- `pcoder runtime bootstrap` — download/install Windows VM runtime payload -- `pcoder setup --init` — first-run onboarding settings -- `pcoder setup --codex-auth <oauth|api> --claude-auth <oauth|api>` — configure auth modes -- `pcoder auth status|login|logout <tool>` — manage auth sessions -- `pcoder run <tool>` — launch selected CLI with normalized env -- `pcoder profile use <name>` — switch active profile -- `pcoder run <tool> --mode linux-portable|host-native` -- `pcoder run <tool> --mode linux-portable --no-sync-back` (skip VM-to-host sync after run) - -## MVP Tool Scope -- `codex` -- `claude` - -## Windows Integration (optional) -- Context-menu bootstrap script -- Portable launch `.cmd` wrappers - -## Non-goals (v0.1) -- Building a new coding model runtime from scratch -- Replacing provider CLIs -- Multi-user credential management server diff --git a/docs/DOCS_INDEX.md b/docs/DOCS_INDEX.md deleted file mode 100644 index 8bc11d3..0000000 --- a/docs/DOCS_INDEX.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Portable Coder Documentation Index - -This folder is the operational source of truth for architecture, planning, quality, and security. - -## Core Control Documents -- `ARCHITECTURE.md` -- `DESIGN.md` -- `QUALITY.md` -- `SECURITY.md` -- `PLANS.md` -- `RUNBOOKS.md` -- `RUNTIME_WINDOWS_VM_BACKEND.md` - -## Governance Artifacts -- `DECISIONS_LOG.md` -- `OPEN_QUESTIONS.md` -- `RISK_REGISTER.md` - -## Execution Plan System -- Active work: `exec-plans/active/` -- Intake notes: `exec-plans/build_required/` -- Completed plans: `exec-plans/completed/` -- Plan debt tracker: `exec-plans/tech-debt-tracker.md` diff --git a/docs/HANDOFF_2026-02-18.md b/docs/HANDOFF_2026-02-18.md deleted file mode 100644 index 4b3e1a5..0000000 --- a/docs/HANDOFF_2026-02-18.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# PortableCoder Handoff (2026-02-18) - -## Current State -- Repo: `https://github.com/Mike-Jenkins-Org/PortableCoder` -- Branch: `main` -- Latest commit: `e34be47` (`Add Windows runtime bootstrap and cloud-init guest seeding`) -- MVP scope remains: Windows-first, Codex + Claude, OAuth + API modes. - -## What Is Done -- Core launcher and planning/harness docs are in place. -- `pcoder` supports: - - `setup`, `auth`, `doctor`, `runtime probe`, `runtime bootstrap`, `run`. -- Windows VM runtime path is implemented: - - QEMU start with WHPX -> TCG fallback. - - SCP sync to/from guest. - - SSH execution in guest. -- Windows smoke-check exists: - - `scripts\runtime\windows\smoke-check.cmd` -- Runtime bootstrap added (new): - - `scripts\runtime\windows\bootstrap-runtime.cmd` - - Downloads QEMU installer + Ubuntu image. - - Generates SSH keypair in `runtime\linux\ssh`. - - VM start now seeds cloud-init from a local metadata server. -- GitHub setup completed: - - Repo is in `Mike-Jenkins-Org`. - - CI workflow on Depot: `.github/workflows/ci.yml` - - Claude PR runner on Depot: `.github/workflows/claude-pr-runner.yml` - - Releases created: `v0.1.0`, `v0.1.1`. - -## Last Test Result (Before Bootstrap Fix) -- On Windows, smoke-check failed due to missing runtime artifacts: - - `runtime\qemu\qemu-system-x86_64.exe` - - `runtime\linux\images\ubuntu.qcow2` - - `runtime\linux\ssh\id_ed25519` -- This is expected if bootstrap has not been run. - -## Next Steps (Resume Here) -1. Pull latest: - - `git pull` -2. Bootstrap runtime payload: - - `scripts\runtime\windows\bootstrap-runtime.cmd` -3. Run smoke-check: - - `scripts\runtime\windows\smoke-check.cmd` -4. Run tool launches in linux-portable mode: - - `scripts\pcoder run codex --mode linux-portable` - - `scripts\pcoder run claude --mode linux-portable` -5. If needed, initialize/settings: - - `scripts\pcoder setup --init` - - `scripts\pcoder setup --codex-auth oauth --claude-auth oauth` - -## If Something Fails -- Bootstrap issues: - - Verify internet access to QEMU and Ubuntu image URLs. - - Optional override env vars: - - `PCODER_QEMU_INSTALLER_URL` - - `PCODER_QEMU_SHA512_URL` - - `PCODER_UBUNTU_IMAGE_URL` -- VM startup/smoke issues: - - Check logs under `state\vm\`: - - `qemu.log` - - `qemu.err.log` - - `qemu-mode.txt` - - `ssh-port.txt` -- Cleanup/stop VM: - - `scripts\runtime\windows\stop-vm.cmd` - diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md deleted file mode 100644 index 13ad3c0..0000000 --- a/docs/OPEN_QUESTIONS.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Open Questions - -| QID | Status | Question | Why It Matters | Decision ID | Owner | Needed By | -|---|---|---|---|---|---|---| -| Q1 | Resolved (2026-02-18) | Should v0.1 ship Windows-only first, or Windows+macOS+Linux together? | Scope and delivery risk | D-001 | Mike | EP-001 kickoff | -| Q2 | Resolved (2026-02-18) | For GLM and MiniMax, do we require native CLIs in MVP or allow provider compatibility routing first? | Impacts adapter and test scope | D-002 | Mike | EP-001 kickoff | -| Q3 | Resolved (2026-02-18) | How should secrets be handled on removable storage (plain env file, encrypted file, or external injection only)? | Security and UX tradeoff | D-003 | Mike | EP-001 build step 1 | -| Q4 | Resolved (2026-02-18) | Do you want repo-distributed runtime binaries, or download/bootstrap on first run? | Repo size and compliance impact | D-004 | Mike | EP-001 build step 1 | -| Q5 | Open | What update policy should we target in MVP? | Operability across machines | D-005 | Mike | EP-002 planning | -| Q6 | Resolved (2026-02-18) | What Linux runtime backend policy should we enforce regarding WSL? | Determines portability and host prerequisites | D-006 | Mike | EP-001 kickoff | -| Q7 | Resolved (2026-02-18, 24.04 LTS) | Which Ubuntu baseline should we target first (22.04 LTS vs 24.04 LTS)? | Affects package compatibility and support window | D-007 | Mike | EP-001 kickoff | -| Q8 | Resolved (2026-02-18) | Which no-install Linux backend should be primary on Windows hosts (bundled QEMU VM vs alternative)? | Defines implementation path for Windows 11 + Server hosts | D-008 | Mike | EP-001 runtime design | -| Q9 | Resolved (2026-02-18, yes) | For Windows Server 2016, is reduced-performance software virtualization acceptable when hardware acceleration is unavailable? | Determines support expectations and acceptance criteria | D-009 | Mike | EP-001 runtime design | -| Q10 | Resolved (2026-02-18, dual mode) | Should MVP auth support OAuth only, API only, or both with persisted onboarding settings? | Determines onboarding UX and secret handling boundaries | D-010 | Mike | EP-001 implementation | diff --git a/docs/PLANS.md b/docs/PLANS.md deleted file mode 100644 index bc7571a..0000000 --- a/docs/PLANS.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# ExecPlans - -ExecPlans are mandatory, living execution contracts for implementation work. - -## Plan Directories -- Active: `docs/exec-plans/active/` -- Intake: `docs/exec-plans/build_required/` -- Completed: `docs/exec-plans/completed/` -- Debt tracker: `docs/exec-plans/tech-debt-tracker.md` - -## Non-Negotiable Requirements -- Self-contained: a new contributor can execute with only repo + plan. -- Living state: update plan during execution, not after. -- Behavior-first: acceptance criteria must be observable. -- Safety-first: include idempotence and recovery notes. - -## Required Sections in Every ExecPlan -- Purpose / Big Picture -- Progress -- Context and Orientation -- Plan of Work -- Concrete Steps -- Validation and Acceptance -- Idempotence and Recovery -- Surprises & Discoveries -- Decision Log -- Outcomes & Retrospective - -## Execution Discipline -1. No implementation without a plan in `active/`. -2. Add timestamps on progress updates. -3. Record decisions and surprises as they happen. -4. Move completed plans to `completed/` after closeout. -5. Convert intake notes to full plans before implementation. diff --git a/docs/QUALITY.md b/docs/QUALITY.md deleted file mode 100644 index 94b1e33..0000000 --- a/docs/QUALITY.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Quality - -## Quality Principles -- Portable-first: no host dependency assumptions. -- Reproducible setup: bootstrap steps produce same layout every time. -- Auditability: config + state layout is understandable and inspectable. - -## v0.1 Quality Gates -- Bootstrap script creates complete expected directory tree. -- Windows runtime bootstrap installs required VM artifacts (`qemu-system-x86_64.exe`, `ubuntu.qcow2`, SSH key pair). -- `pcoder doctor` detects missing runtime binaries and credentials cleanly. -- Tool launch works for at least Codex and Claude in same bundle. -- Windows launcher path handling works with spaces in project paths. -- Windows VM smoke check passes: `scripts/runtime/windows/smoke-check.cmd`. -- GitHub Actions CI executes on Depot runner `depot-ubuntu-24.04`. - -## Future Automation Targets -- Add harness lint for required docs metadata and plan presence. -- Add macOS/Linux smoke scripts to mirror Windows checks. diff --git a/docs/RISK_REGISTER.md b/docs/RISK_REGISTER.md deleted file mode 100644 index 0b12553..0000000 --- a/docs/RISK_REGISTER.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Risk Register - -Scales: Probability 1-5, Impact 1-5 - -| Risk | Prob | Impact | Mitigation | Owner | Review Cadence | -|---|---:|---:|---|---|---| -| Provider CLI changes break wrappers | 4 | 4 | Pin versions, add doctor checks, smoke tests | Mike | Biweekly | -| Credential leakage from portable media | 3 | 5 | External API key injection policy, OAuth state isolated under `state/auth`, clear operator guidance for removable-media handling | Mike | Weekly | -| Windows path/quoting breakage | 4 | 4 | argv spawning, path tests with spaces | Mike | Weekly | -| Runtime bundle size becomes unmanageable | 3 | 3 | staged downloads and selective tool install | Mike | Biweekly | -| Scope creep across too many tools in MVP | 4 | 4 | lock v0.1 tools and defer optional native adapters | Mike | Weekly | -| Windows Server 2016 performance may be reduced under software virtualization fallback | 4 | 3 | try hardware acceleration first, auto-fallback to software mode, document expected tradeoff | Mike | Weekly | diff --git a/docs/RUNBOOKS.md b/docs/RUNBOOKS.md deleted file mode 100644 index 3397958..0000000 --- a/docs/RUNBOOKS.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-20 ---- - -# Runbooks - -## Project Bootstrap -1. Clone/copy repository to target machine. -2. On Windows, run `scripts/runtime/windows/bootstrap-runtime.cmd` to populate VM runtime assets. - - Optional URL overrides: - - `PCODER_QEMU_INSTALLER_URL` - - `PCODER_QEMU_SHA512_URL` - - `PCODER_UBUNTU_IMAGE_URL` -3. Run first-time onboarding: `pcoder setup --init`. -4. Set auth mode per tool: - - `pcoder setup --codex-auth oauth --claude-auth oauth` - - or `pcoder setup --codex-auth api --claude-auth api` -5. Inject credentials only when needed from shell/session environment for API-mode tools. -6. Run `pcoder doctor`. -7. Run `pcoder runtime probe` to see Linux runtime backend options on this host. - -Example env injection: -```bash -export OPENAI_API_KEY=replace_me -export ANTHROPIC_AUTH_TOKEN=replace_me -``` - -## Runtime Modes -- `linux-portable` (primary on Windows): launch tools inside bundled Linux runtime. -- `linux-wsl` (optional): use host WSL when present. -- `host-native` (dev fallback): launch available local tool binaries. -- Runtime bootstrap command: `pcoder runtime bootstrap` (Windows only). - -Current implementation note: -- `linux-portable` execution path is currently implemented for Windows hosts. -- Non-Windows hosts should use `--mode host-native` for now. -- `linux-wsl` mode is planned but not yet implemented in `pcoder run`. - -Backend spec: -- `docs/RUNTIME_WINDOWS_VM_BACKEND.md` -- VM acceleration policy: try hardware acceleration first, auto fallback to portable software mode. - -## Windows Host Matrix (MVP) -- Windows 11: primary target -- Windows Server 2022: target -- Windows Server 2025: target -- Windows Server 2016: target (with software virtualization fallback accepted) - -## VM Lifecycle Scripts -- Runtime bootstrap: `scripts/runtime/windows/bootstrap-runtime.cmd` -- Start VM: `scripts/runtime/windows/start-vm.cmd` -- Start VM implementation: `scripts/runtime/windows/start-vm.ps1` -- Stop VM: `scripts/runtime/windows/stop-vm.cmd` -- Smoke checklist: `scripts/runtime/windows/smoke-check.cmd` -- Smoke checklist (force TCG): `scripts/runtime/windows/smoke-check-tcg.cmd` - -## Linux-Portable Run Flow -1. `pcoder` starts/ensures VM via `start-vm.cmd`. -2. VM launcher attempts WHPX acceleration, then auto-fallback to TCG. -3. On first boot, cloud-init installs Node.js, `codex`, and `claude` CLI tools (may take several minutes). -4. `pcoder run` waits for cloud-init to complete before running the tool. -5. Project is copied into guest over SCP. -6. Tool command runs over SSH in guest project directory. -7. Project is copied back to host after run (unless `--no-sync-back`). - -**First-boot note:** The first time you boot the VM, cloud-init provisions the guest (creates the `portable` user, installs Node.js and the coding CLI tools). This can take 5–15 minutes depending on network speed and host performance. `pcoder run` automatically waits for cloud-init to finish before launching the tool. Subsequent boots skip provisioning and start much faster. - -**Smoke check with tool checks:** Run `scripts/runtime/windows/smoke-check.cmd` after the first boot completes to verify that `codex` and `claude` are installed in the guest. Use `-SkipToolChecks` to skip tool presence checks if only testing VM boot and SSH connectivity. - -Acceleration override (Windows troubleshooting): -- `PCODER_VM_ACCEL_MODE=auto` (default): try WHPX then fallback to TCG if launch fails. -- `PCODER_VM_ACCEL_MODE=whpx`: force WHPX only. -- `PCODER_VM_ACCEL_MODE=tcg`: force software virtualization when WHPX boot/SSH is unreliable. - -## Auth Operations -- Show auth modes and portable auth paths: `pcoder auth status` -- Login with OAuth in current default mode: `pcoder auth login codex`, `pcoder auth login claude` -- Logout in current default mode: `pcoder auth logout codex`, `pcoder auth logout claude` -- In API mode, auth login is optional and API keys are injected from the shell environment. - -## First-Run Validation (Target) -- Windows VM smoke check: `scripts/runtime/windows/smoke-check.cmd` -- Windows: launch via `.cmd` wrapper and from terminal. -- macOS/Linux: launch via shell wrapper. -- Validate project path argument handling with spaces. -- Validate selected runtime mode is reflected in diagnostics output. - -## Local Preflight (Before Windows VM Smoke) -Use this deterministic preflight to validate launcher wiring before VM testing. Windows VM smoke remains `scripts/runtime/windows/smoke-check.cmd`. - -1. Syntax + onboarding checks: -```bash -node --check scripts/pcoder.cjs -scripts/pcoder setup --init --show -``` -2. Doctor with stubbed runners: -```bash -PCODER_CODEX_CMD=node PCODER_CLAUDE_CMD=node scripts/pcoder doctor -``` -3. API-mode host-native launch checks with stubbed runners: -```bash -scripts/pcoder setup --codex-auth api --claude-auth api --windows-mode host-native --show -OPENAI_API_KEY=pcoder-preflight PCODER_CODEX_CMD=node scripts/pcoder run codex --mode host-native -- --version -ANTHROPIC_AUTH_TOKEN=pcoder-preflight PCODER_CLAUDE_CMD=node scripts/pcoder run claude --mode host-native -- --version -``` - -## Incident Recovery -- If runtime corruption is detected, clear `runtime/` and rerun bootstrap. -- If profile corruption is detected, restore `profiles/profiles.json` from version control. -- If provider auth fails, rotate and re-inject keys. diff --git a/docs/RUNTIME_WINDOWS_VM_BACKEND.md b/docs/RUNTIME_WINDOWS_VM_BACKEND.md deleted file mode 100644 index 40428c8..0000000 --- a/docs/RUNTIME_WINDOWS_VM_BACKEND.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Windows VM Backend (Portable Linux Runtime) - -This document defines the MVP backend for Windows-hosted portable Linux execution. - -## Goal -Run Codex and Claude Code inside a Linux guest from a portable folder/zip, with no WSL requirement. - -## Backend Choice -- Primary backend: bundled QEMU VM -- Guest OS: Ubuntu 24.04 LTS -- WSL usage: optional only, never required - -## Portable Folder Layout (Planned) -```text -PortableCoder/ - scripts/ - pcoder.cmd - pcoder - pcoder.cjs - runtime/windows/start-vm.cmd - runtime/windows/stop-vm.cmd - runtime/ - qemu/ - qemu-system-x86_64.exe - qemu-img.exe - *.dll - linux/ - images/ - ubuntu.qcow2 - cloud-init/ - user-data - meta-data - ssh/ - id_ed25519 - id_ed25519.pub - vm-manifest.json - apps/ - codex/ - claude/ - state/ - vm/ - qemu.pid - qemu.log - ssh-port.txt -``` - -## Launch Model -1. Runtime assets are installed via `scripts/runtime/windows/bootstrap-runtime.cmd`. -2. Host launcher checks runtime assets (`runtime/qemu/*`, `runtime/linux/images/*`). -3. Launcher starts QEMU guest with: - - acceleration attempt: `whpx` first - - automatic fallback: `tcg` if acceleration is unavailable/fails - - fixed SSH forwarding from random available local port - - NoCloud-Net cloud-init seed served from host for deterministic guest user/key setup - - state/log outputs under `state/vm/` -4. `pcoder run codex|claude --mode linux-portable` syncs project to guest via SCP, executes via SSH, then syncs back. -5. Stop flow gracefully shuts down guest and cleans stale PID metadata. -6. `scripts/runtime/windows/smoke-check.cmd` validates artifacts, VM boot mode, SSH readiness, and guest tool availability. - -## Host Compatibility Notes -- Windows 11 / Server 2022 / Server 2025: target hosts. -- Server 2016: supported with software virtualization fallback accepted. -- Missing hardware acceleration should degrade to slower software mode, not hard fail. - -## Security Notes -- Secrets are externally injected into host session at launch time. -- Do not persist provider secrets in VM disk image defaults. -- VM image updates should be versioned and checksummed. -- OAuth state is scoped to portable paths (`state/auth/*` on host and `/home/portable/.pcoder-auth/*` in guest). - -## Acceptance Targets (MVP) -- Boot Linux guest from portable folder on supported Windows hosts. -- Run `codex` and `claude` from inside guest. -- No dependency on host WSL installation. -- Deterministic diagnostics for missing VM assets or launch failures. diff --git a/docs/SECURITY.md b/docs/SECURITY.md deleted file mode 100644 index 33d80bc..0000000 --- a/docs/SECURITY.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Security - -Threat model and operational safeguards for portable multi-provider CLI execution. - -## Secret Handling -- API keys are environment-based and must never be committed. -- MVP policy is external injection only: do not store provider secrets in repo or portable bundle files. -- Launch sessions should inject keys at runtime (`OPENAI_API_KEY`, `ANTHROPIC_AUTH_TOKEN`). -- OAuth mode stores provider session state under portable `state/auth/<tool>/` (host) and VM-local `/home/portable/.pcoder-auth/<tool>/`; treat portable media as sensitive when OAuth is enabled. - -## Execution Safety -- Prefer argv-based process spawn over shell string interpolation. -- Validate project path inputs before passing to tool launchers. -- Keep mutation operations explicit; avoid hidden auto-apply behavior. - -## Supply Chain -- Pin runtime/tool versions in lock manifests where possible. -- Document source URLs and checksums for bundled binaries. -- Maintain third-party license notices for redistributed artifacts. - -## Portable Device Risk -- Assume removable media can be lost. -- External injection only is the default mitigation for MVP. diff --git a/docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md b/docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md deleted file mode 100644 index 1bcf804..0000000 --- a/docs/exec-plans/active/EP-001-portable-coder-foundation-and-multi-provider-mvp.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-20 ---- - -# EP-001 - Portable Coder Foundation and Multi-Provider MVP - -## Purpose / Big Picture -Stand up a harness-first foundation and deliver a portable coder MVP that can run multiple coding CLIs from a machine-local folder (or removable media) without host-level installation steps. -Primary runtime direction: portable Linux (Ubuntu-like) execution where feasible, with host-native fallback. -Target Windows hosts for MVP: Windows 11, Server 2016, Server 2022, Server 2025. - -Initial user target providers/tools: -- Codex -- Claude Code -- Deferred post-MVP: Kimi, GLM, MiniMax and others - -## Progress -- [x] (2026-02-18) Created planning control-plane docs and governance artifacts -- [x] (2026-02-18) Create repository runtime skeleton (`runtime/`, `apps/`, `profiles/`, `state/`, `scripts/`) -- [x] (2026-02-18) Implement `pcoder` launcher and adapter contract -- [x] (2026-02-18) Implement `pcoder doctor` checks and provider profile loading -- [x] (2026-02-18) Add Windows `.cmd` and POSIX shell entrypoints -- [x] (2026-02-18) Seed profile and adapter catalog scaffolding, then narrow MVP scope to Codex/Claude -- [x] (2026-02-18) Validate launcher commands (`--help`, `list-tools`, `doctor`) on current host -- [x] (2026-02-18) Add runtime capability probing (`pcoder runtime probe`) and `runtime/linux/` placeholder layout -- [x] (2026-02-18) Decide MVP platform scope: Windows-first -- [x] (2026-02-18) Decide MVP tool scope: Codex + Claude only -- [x] (2026-02-18) Decide secrets policy: external injection only -- [x] (2026-02-18) Decide distribution policy: pre-bundled portable artifact -- [x] (2026-02-18) Decide WSL policy: optional only, never required -- [x] (2026-02-18) Decide no-install Windows Linux backend: bundled QEMU VM -- [x] (2026-02-18) Decide Ubuntu baseline: 24.04 LTS -- [x] (2026-02-18) Accept Server 2016 software virtualization fallback when acceleration is unavailable -- [x] (2026-02-18) Add Windows VM backend blueprint and script scaffolding -- [x] (2026-02-18) Finalize remaining MVP scope decisions (Q7, Q9) -- [x] (2026-02-18) Design Linux runtime backend contract and host fallback policy -- [x] (2026-02-18) Implement `pcoder run --mode linux-portable` flow (VM start, SSH execution, SCP sync) -- [x] (2026-02-18) Implement onboarding/settings and dual auth mode support (`pcoder setup`, `pcoder auth`, persistent portable state) -- [x] (2026-02-18) Add Windows smoke checklist scripts for VM boot/SSH/guest tool validation -- [x] (2026-02-18) Add Windows runtime bootstrap installer (`bootstrap-runtime`) to fetch QEMU/image and generate SSH keys -- [x] (2026-02-20) Validate Codex + Claude launches in one portable layout -- [x] (2026-02-20) Fix Windows bootstrap `ssh-keygen` argument handling for empty-passphrase key generation -- [x] (2026-02-20) Fix Windows VM start cloud-init port selection to fallback when default range is exhausted -- [x] (2026-02-20) Add Windows helper script to patch legacy `start-vm.ps1` cloud-init port fallback on existing local clones -- [x] (2026-02-20) Make Windows smoke SSH probe tolerate transient native-command connection errors during VM boot -- [x] (2026-02-20) Increase VM SSH readiness timeouts and diagnostics for slow first-boot cloud images -- [x] (2026-02-20) Fix Windows smoke SSH probe command invocation so readiness check validates `echo vm-ready` output correctly -- [x] (2026-02-20) Update Windows smoke SSH probe to pass remote commands directly (avoids PowerShell stdin encoding edge cases) -- [x] (2026-02-20) Normalize Windows PowerShell native `ssh` stderr handling so host-key warnings do not abort readiness probes -- [x] (2026-02-20) Add Windows VM acceleration override (`PCODER_VM_ACCEL_MODE`) to force TCG when WHPX guests fail to reach SSH readiness -- [x] (2026-02-20) Add `smoke-check-tcg.cmd` helper for one-command Windows smoke validation in forced software mode -- [ ] (2026-02-18) Document setup/runbook and close out EP-001 - -## Context and Orientation -Control docs and planning model: -- `AGENTS.md` -- `HARNESS_CHECKLIST.md` -- `docs/PLANS.md` -- `docs/ARCHITECTURE.md` -- `docs/SECURITY.md` - -Primary build targets for EP-001: -- `scripts/pcoder` (main entrypoint) -- `scripts/pcoder.cmd` (Windows launcher) -- `scripts/pcoder.cjs` (launcher runtime) -- `scripts/runtime/windows/start-vm.cmd` -- `scripts/runtime/windows/start-vm.ps1` -- `scripts/runtime/windows/stop-vm.cmd` -- `scripts/adapters/*` -- `profiles/defaults/*` -- `runtime/linux/*` (planned) -- `runtime/linux/vm-manifest.json` -- `docs/RUNTIME_WINDOWS_VM_BACKEND.md` -- `docs/RUNBOOKS.md` - -## Plan of Work -### A) Lock scope and safety constraints -Scope decisions complete for EP-001. Maintain decision artifacts as implementation evolves. - -### B) Build runtime and adapter framework -Create deterministic portable directory structure and launcher contract with provider-specific env translation. - -### C) Implement MVP tool paths -Enable Codex + Claude as required first-class launchers. - -### D) Validate and harden -Run smoke validations, document recovery steps, and capture follow-up tech debt. - -### E) Linux runtime strategy -Define and prototype first Linux runtime backend with deterministic fallback behavior. - -## Concrete Steps -1. Keep decision records synchronized with runtime implementation updates. -2. Scaffold runtime directories and placeholder manifests. -3. Implement `pcoder` command surface: - - `doctor` - - `list-tools` - - `run <tool>` - - `profile use <name>` -4. Implement adapter modules for Codex and Claude. -5. Add Windows + POSIX wrappers and project-path argument handling. -6. Design and prototype Linux runtime mode (`linux-portable`) with no hard WSL dependency. -7. Add initial smoke scripts and update `docs/RUNBOOKS.md`. - -## Validation and Acceptance -Acceptance criteria for EP-001: -- Planning system is in place and usable for future EPs. -- Portable launcher can invoke at least Codex and Claude using runtime env injection. -- `pcoder doctor` identifies missing runtime/tools/config with actionable errors. -- Windows and POSIX wrappers handle project paths that include spaces. -- Linux runtime architecture decision is captured with clear implementation path. -- Linux backend path works without requiring WSL preinstallation. - -## Idempotence and Recovery -- Re-running scaffolding should not duplicate or corrupt existing profile files. -- Missing or bad profile files should produce deterministic diagnostics. -- Runtime can be rebuilt by clearing `runtime/` and rerunning bootstrap. - -## Surprises & Discoveries -- 2026-02-18: Product direction expanded to prefer portable Linux runtime (Ubuntu-like) with host-native fallback. -- 2026-02-18: Host-level Node module type (`type: module`) from parent directory required launcher migration from `pcoder.js` to `pcoder.cjs`. -- 2026-02-18: User requires portable operation on Windows hosts without making WSL a prerequisite. -- 2026-02-18: MVP project handoff uses SCP sync in/out of VM instead of shared-folder mounts to reduce host dependency assumptions. -- 2026-02-18: OAuth + API dual-mode support required explicit onboarding state and per-tool auth-mode persistence. -- 2026-02-18: Profile resolution needed per-tool default fallback to avoid cross-provider env validation errors in API mode. -- 2026-02-20: Host-native and stubbed CI-equivalent validation passed on Linux host for Codex + Claude launch flows. -- 2026-02-20: Windows VM smoke validation remains target-specific and is still tracked separately from Linux-host checks. -- 2026-02-20: PowerShell `Start-Process -ArgumentList` rejected empty elements, so `ssh-keygen -N` required explicit empty-string handling. -- 2026-02-20: Some Windows hosts had no free ports in `38080-38120`; cloud-init server startup now needs fallback port allocation. -- 2026-02-20: On some PowerShell environments, transient `ssh` connection failures surfaced as exceptions; smoke probing must retry instead of aborting early. -- 2026-02-20: First boot SSH readiness on some hosts can exceed 120 seconds, so timeout windows need to be configurable and longer by default. -- 2026-02-20: Passing `ssh ... bash -lc <script>` as split args can drop expected output semantics; piping script content to `bash -s` is safer for deterministic probing. -- 2026-02-20: Piping probe scripts into native `ssh` from Windows PowerShell can hit stdin encoding edge cases; direct remote command args are more reliable. -- 2026-02-20: In Windows PowerShell, native stderr can surface as terminating `ErrorRecord` when `$ErrorActionPreference='Stop'`, so probe wrappers must normalize stderr explicitly. -- 2026-02-20: Some hosts can launch QEMU with WHPX but still fail guest SSH readiness; explicit TCG override is needed for deterministic recovery. - -## Decision Log -- 2026-02-18: Adopt harness-first planning model before implementation. -- 2026-02-18: Treat portable Linux runtime as preferred execution target for the product. -- 2026-02-18: Lock MVP scope to Windows only; macOS/Linux support deferred to follow-up EP. -- 2026-02-18: Lock MVP providers to Codex + Claude; defer others. -- 2026-02-18: Enforce external secret injection only (no stored secrets in portable artifact). -- 2026-02-18: Use pre-bundled distribution artifacts for machine-to-machine portability. -- 2026-02-18: WSL may be used if present but cannot be a runtime requirement. -- 2026-02-18: Primary Windows backend will be bundled QEMU VM for no-install Linux execution. -- 2026-02-18: Ubuntu 24.04 LTS selected as guest baseline. -- 2026-02-18: Runtime should try hardware acceleration first, then auto-fallback to portable software mode. - -## Outcomes & Retrospective -- Pending. diff --git a/docs/exec-plans/build_required/README.md b/docs/exec-plans/build_required/README.md deleted file mode 100644 index 72077f3..0000000 --- a/docs/exec-plans/build_required/README.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Build Required Intake - -This folder is for raw idea notes not yet converted into full ExecPlans. - -## Workflow -1. Add/edit intake notes here. -2. Convert each into one or more full ExecPlans in `docs/exec-plans/active/`. -3. Move processed notes to `docs/exec-plans/completed/` archive area. diff --git a/docs/exec-plans/completed/README.md b/docs/exec-plans/completed/README.md deleted file mode 100644 index bb02f4b..0000000 --- a/docs/exec-plans/completed/README.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# Completed ExecPlans - -Store closed execution plans here with outcomes and validation evidence. diff --git a/docs/exec-plans/tech-debt-tracker.md b/docs/exec-plans/tech-debt-tracker.md deleted file mode 100644 index 9b9f094..0000000 --- a/docs/exec-plans/tech-debt-tracker.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -owner: Mike Jenkins -last_verified: 2026-02-18 ---- - -# ExecPlan Tech Debt Tracker - -| Debt ID | Description | Source Plan | Priority | Status | -|---|---|---|---|---| -| TD-001 | Add automated harness lint checks for metadata + plan hygiene | EP-001 | Medium | Open | diff --git a/profiles/profiles.json b/profiles/profiles.json index 85ebed0..1b6512d 100644 --- a/profiles/profiles.json +++ b/profiles/profiles.json @@ -1,13 +1,6 @@ { - "default_profile": "openai", + "default_profile": "anthropic", "profiles": { - "openai": { - "description": "OpenAI/Codex profile (external env injection only)", - "env_files": [], - "required_env": [ - "OPENAI_API_KEY" - ] - }, "anthropic": { "description": "Claude/Anthropic profile (external env injection only)", "env_files": [], diff --git a/scripts/adapters/catalog.json b/scripts/adapters/catalog.json index 43ea278..15011e9 100644 --- a/scripts/adapters/catalog.json +++ b/scripts/adapters/catalog.json @@ -1,10 +1,4 @@ { - "codex": { - "display_name": "Codex CLI", - "default_profile": "openai", - "command_env": "PCODER_CODEX_CMD", - "candidate_commands": ["codex"] - }, "claude": { "display_name": "Claude Code CLI", "default_profile": "anthropic", diff --git a/scripts/pcoder.cjs b/scripts/pcoder.cjs index d04c81c..be1ac38 100755 --- a/scripts/pcoder.cjs +++ b/scripts/pcoder.cjs @@ -8,17 +8,14 @@ const crypto = require('crypto'); const repoRoot = path.resolve(__dirname, '..'); const stateDir = path.join(repoRoot, 'state'); -const profilesPath = path.join(repoRoot, 'profiles', 'profiles.json'); -const adaptersPath = path.join(repoRoot, 'scripts', 'adapters', 'catalog.json'); -const activeProfilePath = path.join(stateDir, 'active-profile.txt'); +const settingsPath = path.join(stateDir, 'settings.json'); +const authStateRoot = path.join(stateDir, 'auth'); const vmManifestPath = path.join(repoRoot, 'runtime', 'linux', 'vm-manifest.json'); const vmStateDir = path.join(repoRoot, 'state', 'vm'); const vmSshPortPath = path.join(vmStateDir, 'ssh-port.txt'); -const settingsPath = path.join(stateDir, 'settings.json'); -const authStateRoot = path.join(stateDir, 'auth'); -const authModeValues = new Set(['oauth', 'api']); const runModeValues = new Set(['linux-portable', 'host-native', 'linux-wsl']); const windowsDefaultModeValues = new Set(['linux-portable', 'host-native']); +const authModeValues = new Set(['oauth', 'api']); function main(argv) { const [command, ...rest] = argv; @@ -26,12 +23,6 @@ function main(argv) { case 'doctor': commandDoctor(); return; - case 'list-tools': - commandListTools(); - return; - case 'profile': - commandProfile(rest); - return; case 'setup': commandSetup(rest); return; @@ -47,38 +38,52 @@ function main(argv) { case 'help': case '--help': case '-h': - case undefined: printHelp(); return; + case undefined: + // No args: launch Claude in current directory (host-native) + commandRun([]); + return; default: - fail(`Unknown command: ${command}`); + // Pass unrecognized first argument (and remaining args) directly to claude. + // This allows patterns like `pcoder --resume` or `pcoder --print "hello"`. + // If you mistyped a command (e.g. 'pcoder doctro'), claude will receive it as an arg. + commandRun([command, ...rest]); } } function printHelp() { - const adapters = loadJsonSafe(adaptersPath, 'adapter catalog'); - const toolNames = Object.keys(adapters); - console.log('Portable Coder Launcher (pcoder)'); + console.log('Portable Claude Code Launcher'); console.log(''); console.log('Usage:'); - console.log(' pcoder doctor'); - console.log(' pcoder list-tools'); - console.log(' pcoder profile use <name>'); - console.log(' pcoder profile show'); - console.log(' pcoder setup [--init] [--codex-auth <oauth|api>] [--claude-auth <oauth|api>] [--windows-mode <linux-portable|host-native>] [--sync-back <true|false>] [--show]'); - console.log(' pcoder auth status'); - console.log(' pcoder auth login <codex|claude> [--mode <linux-portable|host-native>]'); - console.log(' pcoder auth logout <codex|claude> [--mode <linux-portable|host-native>]'); - console.log(' pcoder runtime probe'); - console.log(' pcoder runtime bootstrap [--force]'); - console.log(' pcoder run <tool> [--mode <linux-portable|host-native>] [--profile <name>] [--project <path>] [--no-sync-back] [-- <tool args...>]'); + console.log(' pcoder Launch Claude Code in current directory'); + console.log(' pcoder [-- <claude args...>] Launch Claude Code with extra args'); + console.log(' pcoder doctor Check environment health'); + console.log(' pcoder setup [--init] Initialize or show settings'); + console.log(' [--claude-auth <oauth|api>]'); + console.log(' [--windows-mode <linux-portable|host-native>]'); + console.log(' [--sync-back <true|false>]'); + console.log(' [--show]'); + console.log(' pcoder auth status Show auth status'); + console.log(' pcoder auth login Log in via OAuth'); + console.log(' pcoder auth logout Log out'); + console.log(' pcoder runtime probe Probe available runtimes'); + console.log(' pcoder runtime bootstrap Download/install Windows VM runtime'); + console.log(' pcoder run [--mode <linux-portable|host-native>]'); + console.log(' [--project <path>] [--no-sync-back] [-- <claude args...>]'); + console.log(''); + console.log('Auth modes:'); + console.log(' oauth - use Claude OAuth login (default, credentials stored portably)'); + console.log(' api - inject ANTHROPIC_API_KEY environment variable at launch time'); console.log(''); - console.log(`Tools: ${toolNames.join(', ')}`); + console.log('Windows run modes:'); + console.log(' linux-portable - run Claude inside bundled QEMU Linux VM (default on Windows)'); + console.log(' host-native - run Claude directly on Windows (requires claude in PATH)'); } function commandDoctor() { const checks = []; - const requiredDirs = ['runtime', 'apps', 'profiles', 'state', 'scripts']; + const requiredDirs = ['runtime', 'state', 'scripts', 'profiles']; for (const rel of requiredDirs) { const abs = path.join(repoRoot, rel); checks.push({ @@ -88,9 +93,8 @@ function commandDoctor() { }); } - const profiles = loadJsonSafe(profilesPath, 'profiles manifest'); - const adapters = loadJsonSafe(adaptersPath, 'adapter catalog'); const settings = loadSettings(); + checks.push({ label: 'settings:file', ok: settingsFileExists(), @@ -99,73 +103,41 @@ function commandDoctor() { : "missing (run 'pcoder setup --init')" }); - let activeProfile = readActiveProfile(); - if (!activeProfile) { - activeProfile = profiles.default_profile || ''; - } - - checks.push({ - label: 'active-profile', - ok: Boolean(activeProfile), - detail: activeProfile || '(not set)' - }); - - for (const [name, profile] of Object.entries(profiles.profiles || {})) { - const envResult = loadProfileEnv(profile, false); - const effectiveEnv = { ...process.env, ...envResult.env }; - const req = validateProfileRequirements(profile, effectiveEnv); - const toolForProfile = findToolByDefaultProfile(name, adapters); - const authMode = toolForProfile ? getAuthModeForTool(toolForProfile, settings) : 'api'; - let shouldEnforce = false; - let skippedReason = 'optional profile skipped'; - if (toolForProfile) { - const selectedProfile = resolveProfileName(null, adapters[toolForProfile], profiles); - shouldEnforce = selectedProfile === name && authMode === 'api'; - if (selectedProfile !== name) { - skippedReason = `profile not selected for ${toolForProfile} (selected: ${selectedProfile})`; - } else if (authMode !== 'api') { - skippedReason = `selected for ${toolForProfile} in oauth mode`; - } - } else { - const isActive = name === activeProfile; - shouldEnforce = isActive; - skippedReason = isActive ? 'active profile (no tool mapping)' : 'optional profile skipped'; - } - + const authMode = settings.auth.claude; + if (process.platform === 'win32') { checks.push({ - label: `profile:${name}:env-files`, - ok: shouldEnforce ? envResult.missingFiles.length === 0 : true, - detail: shouldEnforce - ? (envResult.missingFiles.length === 0 ? 'ok' : `missing ${envResult.missingFiles.join(', ')}`) - : (envResult.missingFiles.length > 0 - ? `optional profile not configured (${envResult.missingFiles.join(', ')})` - : 'optional profile uses external env injection') + label: 'tool:claude:runner', + ok: true, + detail: `vm guest runner 'claude' (auth=${authMode}, host binary optional)` }); + } else { + const runner = commandExists('claude') ? 'claude' : (process.env.PCODER_CLAUDE_CMD || null); checks.push({ - label: `profile:${name}:required-env`, - ok: shouldEnforce ? req.ok : true, - detail: shouldEnforce ? req.message : skippedReason + label: 'tool:claude:runner', + ok: Boolean(runner), + detail: runner ? `${runner} (auth=${authMode})` : 'not found (claude not in PATH)' }); } - for (const [tool, adapter] of Object.entries(adapters)) { - const authMode = getAuthModeForTool(tool, settings); - if (process.platform === 'win32') { - const vmRunner = resolveVmToolRunner(tool, adapter, process.env); - checks.push({ - label: `tool:${tool}:runner`, - ok: true, - detail: `vm guest runner '${vmRunner}' (auth=${authMode}, host binary optional)` - }); - continue; - } - - const probeEnv = process.env; - const runner = resolveRunner(adapter, probeEnv); + if (authMode === 'api') { + const hasKey = Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN); checks.push({ - label: `tool:${tool}:runner`, - ok: Boolean(runner), - detail: runner ? `${runner} (auth=${authMode})` : `not found (${adapter.candidate_commands.join(', ')})` + label: 'claude:api-key', + ok: hasKey, + detail: hasKey ? 'ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN set' : 'missing ANTHROPIC_API_KEY (api auth mode)' + }); + } else { + const authPaths = getPortableHostAuthPaths(); + const claudeConfigDir = path.join(authPaths.home, '.claude'); + checks.push({ + label: 'claude:oauth', + ok: true, + detail: `oauth home: ${path.relative(repoRoot, authPaths.home)}` + }); + checks.push({ + label: 'claude:oauth:config-dir', + ok: true, + detail: claudeConfigDir }); } @@ -182,63 +154,19 @@ function commandDoctor() { if (failed > 0) { process.exitCode = 2; console.log(`\nDoctor completed with ${failed} failed check(s).`); - console.log("Run 'pcoder setup --init' for first-time onboarding, then inject required env vars and ensure CLI runners are available."); + console.log("Run 'pcoder setup --init' for first-time setup."); return; } console.log('\nDoctor completed: all checks passed.'); } -function commandListTools() { - const adapters = loadJsonSafe(adaptersPath, 'adapter catalog'); - console.log('Available tools:\n'); - for (const [name, adapter] of Object.entries(adapters)) { - console.log(`- ${name}`); - console.log(` ${adapter.display_name}`); - console.log(` default profile: ${adapter.default_profile}`); - console.log(` command env: ${adapter.command_env}`); - console.log(` candidates: ${adapter.candidate_commands.join(', ')}`); - } -} - -function commandProfile(args) { - const action = args[0]; - const profiles = loadJsonSafe(profilesPath, 'profiles manifest'); - if (action === 'show' || !action) { - const active = readActiveProfile() || profiles.default_profile || '(none)'; - console.log(active); - return; - } - - if (action !== 'use') { - fail(`Unknown profile action: ${action}`); - } - - const name = args[1]; - if (!name) { - fail('Usage: pcoder profile use <name>'); - } - - if (!profiles.profiles[name]) { - const known = Object.keys(profiles.profiles).join(', '); - fail(`Unknown profile '${name}'. Known profiles: ${known}`); - } - - ensureDir(stateDir); - fs.writeFileSync(activeProfilePath, `${name}\n`, 'utf8'); - console.log(`Active profile set to '${name}'.`); -} - function commandSetup(args) { const parsed = parseSetupArgs(args); const hadSettings = settingsFileExists(); const settings = parsed.init ? defaultSettings() : loadSettings(); let changed = parsed.init; - if (parsed.codexAuth) { - changed = changed || settings.auth.codex !== parsed.codexAuth; - settings.auth.codex = parsed.codexAuth; - } if (parsed.claudeAuth) { changed = changed || settings.auth.claude !== parsed.claudeAuth; settings.auth.claude = parsed.claudeAuth; @@ -261,7 +189,7 @@ function commandSetup(args) { console.log('Setup saved to state/settings.json'); console.log(''); } else if (!hadSettings) { - console.log("Settings are not initialized yet. Run 'pcoder setup --init' to create state/settings.json."); + console.log("Settings not initialized yet. Run 'pcoder setup --init' to create state/settings.json."); console.log(''); } printSettings(settings, hadSettings || shouldSave); @@ -271,7 +199,6 @@ function commandAuth(args) { const action = args[0]; const hasSettings = settingsFileExists(); const settings = hasSettings ? loadSettings() : defaultSettings(); - const adapters = loadJsonSafe(adaptersPath, 'adapter catalog'); if (!action || action === 'status') { printAuthStatus(settings, hasSettings); @@ -279,45 +206,32 @@ function commandAuth(args) { } if (action !== 'login' && action !== 'logout') { - fail('Usage: pcoder auth <status|login|logout> [tool] [--mode <linux-portable|host-native>]'); + fail('Usage: pcoder auth <status|login|logout> [--mode <linux-portable|host-native>]'); } if (!hasSettings) { - fail("Settings are not initialized. Run 'pcoder setup --init' before auth login/logout."); - } - - const tool = args[1]; - if (!tool || !adapters[tool]) { - fail(`Unknown or missing tool. Supported: ${Object.keys(adapters).join(', ')}`); + fail("Settings not initialized. Run 'pcoder setup --init' before auth login/logout."); } - const parsed = parseAuthArgs(args.slice(2)); + const parsed = parseAuthArgs(args.slice(1)); const mode = resolveRunMode(parsed.mode, settings); - const authMode = getAuthModeForTool(tool, settings); + const authMode = settings.auth.claude; if (action === 'login' && authMode === 'api') { - console.log(`[warn] ${tool} auth mode is 'api'; OAuth login is optional.`); + console.log('[warn] claude auth mode is api; OAuth login is optional.'); } + const authCommandSettings = authMode === 'oauth' ? settings - : { - ...settings, - auth: { - ...settings.auth, - [tool]: 'oauth' - } - }; - const authExecutionMode = 'oauth'; + : { ...settings, auth: { ...settings.auth, claude: 'oauth' } }; if (mode === 'linux-portable') { runInLinuxPortableVm({ - tool, - adapter: adapters[tool], projectPath: repoRoot, - mergedEnv: applyPortableHostAuthEnv(tool, { ...process.env }, authCommandSettings), + mergedEnv: applyPortableHostAuthEnv({ ...process.env }, authCommandSettings), toolArgs: [action], noSyncBack: true, skipProjectSync: true, - authMode: authExecutionMode, + authMode: 'oauth', settings }); return; @@ -331,11 +245,10 @@ function commandAuth(args) { fail(`Unsupported auth mode target '${mode}'.`); } - const adapter = adapters[tool]; - const env = applyPortableHostAuthEnv(tool, { ...process.env }, authCommandSettings); - const runner = resolveRunner(adapter, env); + const env = applyPortableHostAuthEnv({ ...process.env }, authCommandSettings); + const runner = resolveRunner(env); if (!runner) { - fail(`No executable found for ${tool}. Install it or set ${adapter.command_env}.`); + fail('No claude executable found. Install claude or set PCODER_CLAUDE_CMD.'); } const result = cp.spawnSync(runner, [action], { @@ -344,7 +257,7 @@ function commandAuth(args) { env }); if (result.error) { - fail(`Failed to run ${tool} ${action}: ${result.error.message}`); + fail(`Failed to run claude ${action}: ${result.error.message}`); } process.exitCode = typeof result.status === 'number' ? result.status : 1; } @@ -456,40 +369,18 @@ function recommendRuntimeBackend(platform, probes) { } function commandRun(args) { - const tool = args[0]; - if (!tool) { - fail('Usage: pcoder run <tool> [--mode <linux-portable|host-native>] [--profile <name>] [--project <path>] [--no-sync-back] [-- <tool args...>]'); - } - if (!settingsFileExists()) { - fail("Settings are not initialized. Run 'pcoder setup --init' before running tools."); - } - - const parsed = parseRunArgs(args.slice(1)); - const profiles = loadJsonSafe(profilesPath, 'profiles manifest'); - const adapters = loadJsonSafe(adaptersPath, 'adapter catalog'); + const parsed = parseRunArgs(args); const settings = loadSettings(); - const adapter = adapters[tool]; - - if (!adapter) { - fail(`Unknown tool '${tool}'. Run 'pcoder list-tools'.`); - } - - const profileName = resolveProfileName(parsed.profile, adapter, profiles); - const profile = profiles.profiles[profileName]; - if (!profile) { - fail(`Profile '${profileName}' is not defined in profiles/profiles.json`); - } - const loaded = loadProfileEnv(profile, true); - const mergedEnv = { ...process.env, ...loaded.env }; - const authMode = getAuthModeForTool(tool, settings); - applyPortableHostAuthEnv(tool, mergedEnv, settings); - applyToolCompatibilityEnv(tool, mergedEnv); + const mergedEnv = { ...process.env }; + const authMode = settings.auth.claude; + applyPortableHostAuthEnv(mergedEnv, settings); + applyClaudeCompatibilityEnv(mergedEnv); if (authMode === 'api') { - const req = validateProfileRequirements(profile, mergedEnv); - if (!req.ok) { - fail(`Profile '${profileName}' is missing required env values: ${req.message}`); + const hasKey = Boolean(mergedEnv.ANTHROPIC_API_KEY || mergedEnv.ANTHROPIC_AUTH_TOKEN); + if (!hasKey) { + fail("Claude auth mode is 'api' but ANTHROPIC_API_KEY is not set. Set the env var or switch to oauth with 'pcoder setup --claude-auth oauth'."); } } @@ -502,10 +393,9 @@ function commandRun(args) { const noSyncBack = parsed.noSyncBack === true ? true : !Boolean(settings.runtime.sync_back_default); + if (mode === 'linux-portable') { runInLinuxPortableVm({ - tool, - adapter, projectPath, mergedEnv, toolArgs: parsed.toolArgs, @@ -525,9 +415,12 @@ function commandRun(args) { fail(`Unsupported run mode '${mode}'. Supported modes: linux-portable, host-native`); } - const runner = resolveRunner(adapter, mergedEnv); + const runner = resolveRunner(mergedEnv); if (!runner) { - fail(`No executable found for tool '${tool}'. Set ${adapter.command_env} or install one of: ${adapter.candidate_commands.join(', ')}`); + if (process.platform === 'win32') { + fail("No claude executable found in host-native mode. Either install claude on Windows (npm install -g @anthropic-ai/claude-code), set PCODER_CLAUDE_CMD, or switch to VM mode: pcoder setup --windows-mode linux-portable"); + } + fail("No claude executable found. Install claude (npm install -g @anthropic-ai/claude-code) or set PCODER_CLAUDE_CMD."); } const result = cp.spawnSync(runner, parsed.toolArgs, { @@ -544,18 +437,13 @@ function commandRun(args) { } function parseRunArgs(args) { - const parsed = { profile: null, project: null, mode: null, noSyncBack: false, toolArgs: [] }; + const parsed = { project: null, mode: null, noSyncBack: false, toolArgs: [] }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === '--') { parsed.toolArgs = args.slice(i + 1); return parsed; } - if (arg === '--profile') { - parsed.profile = args[i + 1] || null; - i += 1; - continue; - } if (arg === '--project') { parsed.project = args[i + 1] || null; i += 1; @@ -578,7 +466,6 @@ function parseRunArgs(args) { function parseSetupArgs(args) { const parsed = { init: false, - codexAuth: null, claudeAuth: null, windowsMode: null, syncBack: undefined, @@ -595,12 +482,6 @@ function parseSetupArgs(args) { if (arg === '--show') { continue; } - if (arg === '--codex-auth') { - parsed.codexAuth = normalizeAuthModeValue(args[i + 1], '--codex-auth'); - parsed.persist = true; - i += 1; - continue; - } if (arg === '--claude-auth') { parsed.claudeAuth = normalizeAuthModeValue(args[i + 1], '--claude-auth'); parsed.persist = true; @@ -677,7 +558,6 @@ function defaultSettings() { return { version: 1, auth: { - codex: 'oauth', claude: 'oauth' }, runtime: { @@ -709,7 +589,6 @@ function normalizeSettings(raw) { const settings = { version: defaults.version, auth: { - codex: defaults.auth.codex, claude: defaults.auth.claude }, runtime: { @@ -726,9 +605,6 @@ function normalizeSettings(raw) { if (!raw.auth || typeof raw.auth !== 'object' || Array.isArray(raw.auth)) { fail('settings.auth must be an object when present.'); } - if (raw.auth.codex !== undefined) { - settings.auth.codex = normalizeAuthModeValue(raw.auth.codex, 'settings.auth.codex'); - } if (raw.auth.claude !== undefined) { settings.auth.claude = normalizeAuthModeValue(raw.auth.claude, 'settings.auth.claude'); } @@ -763,7 +639,6 @@ function saveSettings(settings) { function printSettings(settings, initialized) { console.log('Settings'); console.log(` initialized: ${initialized ? 'yes' : 'no'}`); - console.log(` codex auth: ${settings.auth.codex}`); console.log(` claude auth: ${settings.auth.claude}`); console.log(` windows default mode: ${settings.runtime.windows_default_mode}`); console.log(` sync back default: ${settings.runtime.sync_back_default ? 'true' : 'false'}`); @@ -773,40 +648,19 @@ function printSettings(settings, initialized) { function printAuthStatus(settings, initialized) { console.log('Auth status'); console.log(` settings initialized: ${initialized ? 'yes' : 'no'}`); - - const tools = ['codex', 'claude']; - for (const tool of tools) { - const mode = getAuthModeForTool(tool, settings); - const hostPaths = getPortableHostAuthPaths(tool); - console.log(` ${tool}: ${mode}`); - if (mode === 'oauth') { - console.log(` ${tool} host oauth home: ${path.relative(repoRoot, hostPaths.home)}`); - console.log(` ${tool} vm oauth home: /home/portable/.pcoder-auth/${tool}`); - } else { - console.log(` ${tool} api mode: inject provider key env vars at launch time`); - } - } -} - -function findToolByDefaultProfile(profileName, adapters) { - for (const [tool, adapter] of Object.entries(adapters)) { - if (adapter.default_profile === profileName) { - return tool; - } - } - return null; -} - -function getAuthModeForTool(tool, settings) { - const mode = settings && settings.auth ? settings.auth[tool] : null; - if (!mode) { - return 'oauth'; + const mode = settings.auth.claude; + const hostPaths = getPortableHostAuthPaths(); + console.log(` claude: ${mode}`); + if (mode === 'oauth') { + console.log(` claude host oauth home: ${path.relative(repoRoot, hostPaths.home)}`); + console.log(` claude vm oauth home: /home/portable/.pcoder-auth/claude`); + } else { + console.log(` claude api mode: inject ANTHROPIC_API_KEY at launch time`); } - return normalizeAuthModeValue(mode, `settings.auth.${tool}`); } -function getPortableHostAuthPaths(tool) { - const root = path.join(authStateRoot, tool, 'host'); +function getPortableHostAuthPaths() { + const root = path.join(authStateRoot, 'claude', 'host'); const home = path.join(root, 'home'); const config = path.join(home, '.config'); const cache = path.join(home, '.cache'); @@ -815,15 +669,15 @@ function getPortableHostAuthPaths(tool) { return { root, home, config, cache, data, state }; } -function applyPortableHostAuthEnv(tool, env, settings) { - const authMode = getAuthModeForTool(tool, settings); +function applyPortableHostAuthEnv(env, settings) { + const authMode = settings.auth.claude; env.PCODER_AUTH_MODE = authMode; if (authMode !== 'oauth') { return env; } - const authPaths = getPortableHostAuthPaths(tool); + const authPaths = getPortableHostAuthPaths(); ensureDir(authPaths.root); ensureDir(authPaths.home); ensureDir(authPaths.config); @@ -848,20 +702,19 @@ function applyPortableHostAuthEnv(tool, env, settings) { env.LOCALAPPDATA = localAppData; } - if (tool === 'claude') { - const claudeConfigDir = path.join(authPaths.home, '.claude'); - ensureDir(claudeConfigDir); - env.CLAUDE_CONFIG_DIR = claudeConfigDir; - } - if (tool === 'codex') { - const openaiHome = path.join(authPaths.home, '.openai'); - ensureDir(openaiHome); - env.OPENAI_HOME = openaiHome; - } + const claudeConfigDir = path.join(authPaths.home, '.claude'); + ensureDir(claudeConfigDir); + env.CLAUDE_CONFIG_DIR = claudeConfigDir; return env; } +function applyClaudeCompatibilityEnv(env) { + if (!env.ANTHROPIC_AUTH_TOKEN && env.ANTHROPIC_API_KEY) { + env.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_API_KEY; + } +} + function resolveRunMode(explicitMode, settings) { if (explicitMode) { return normalizeRunModeValue(explicitMode, '--mode'); @@ -872,113 +725,19 @@ function resolveRunMode(explicitMode, settings) { return 'host-native'; } -function resolveProfileName(explicit, adapter, profiles) { - if (explicit) { - return explicit; - } - const active = readActiveProfile(); - if (active && (!adapter.default_profile || active === adapter.default_profile)) { - return active; - } - if (adapter.default_profile) { - return adapter.default_profile; - } - if (active) { - return active; - } - return profiles.default_profile; -} - -function loadProfileEnv(profile, requireFiles) { - const env = {}; - const missingFiles = []; - const files = Array.isArray(profile.env_files) ? profile.env_files : []; - - for (const rel of files) { - const abs = path.join(repoRoot, rel); - if (!fs.existsSync(abs)) { - missingFiles.push(rel); - continue; - } - Object.assign(env, parseEnvFile(abs)); - } - - if (requireFiles && missingFiles.length > 0) { - fail(`Missing env file(s): ${missingFiles.join(', ')}. Provide profile files or switch to external env injection.`); - } - - return { env, missingFiles }; -} - -function parseEnvFile(filePath) { - const out = {}; - const text = fs.readFileSync(filePath, 'utf8'); - const lines = text.split(/\r?\n/); - for (const lineRaw of lines) { - const line = lineRaw.trim(); - if (!line || line.startsWith('#')) { - continue; - } - const idx = line.indexOf('='); - if (idx <= 0) { - continue; - } - const key = line.slice(0, idx).trim(); - let value = line.slice(idx + 1).trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - out[key] = value; - } - return out; -} - -function validateProfileRequirements(profile, env) { - const required = Array.isArray(profile.required_env) ? profile.required_env : []; - const missing = []; - for (const key of required) { - if (!env[key]) { - missing.push(key); - } - } - - const requiredAnyGroups = Array.isArray(profile.required_env_any) ? profile.required_env_any : []; - const failedAny = []; - for (const group of requiredAnyGroups) { - const keys = Array.isArray(group) ? group : []; - const hasOne = keys.some((k) => Boolean(env[k])); - if (!hasOne && keys.length > 0) { - failedAny.push(keys); - } - } - - if (missing.length === 0 && failedAny.length === 0) { - return { ok: true, message: 'ok' }; - } - - const parts = []; - if (missing.length > 0) { - parts.push(`missing all of: ${missing.join(', ')}`); - } - for (const keys of failedAny) { - parts.push(`need one of: ${keys.join(' | ')}`); +function resolveRunner(env) { + const override = env.PCODER_CLAUDE_CMD; + if (override) { + return override; } - - return { ok: false, message: parts.join('; ') }; -} - -function applyToolCompatibilityEnv(tool, env) { - if (tool === 'claude') { - if (!env.ANTHROPIC_AUTH_TOKEN && env.ANTHROPIC_API_KEY) { - env.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_API_KEY; - } + if (commandExists('claude')) { + return 'claude'; } + return null; } function runInLinuxPortableVm(options) { const { - tool, - adapter, projectPath, mergedEnv, toolArgs, @@ -1018,7 +777,6 @@ function runInLinuxPortableVm(options) { const remoteProjectPath = skipProjectSync ? (mergedEnv.PCODER_VM_AUTH_WORKDIR || '/home/portable') : buildRemoteProjectPath(remoteRoot, projectPath); - const vmRunner = resolveVmToolRunner(tool, adapter, mergedEnv); const prepLines = ['set -e']; if (skipProjectSync) { @@ -1056,10 +814,8 @@ function runInLinuxPortableVm(options) { } const remoteScript = buildRemoteRunScript({ - tool, authMode, remoteProjectPath, - vmRunner, toolArgs, mergedEnv }); @@ -1171,24 +927,12 @@ function resolveScpCommand(env, sshCmd) { } function waitForVmSshReady(options) { - const { - sshCmd, - sshHost, - sshPort, - sshUser, - sshKeyPath, - timeoutSeconds - } = options; - + const { sshCmd, sshHost, sshPort, sshUser, sshKeyPath, timeoutSeconds } = options; const startedAt = Date.now(); const timeoutMs = timeoutSeconds * 1000; while ((Date.now() - startedAt) < timeoutMs) { const probe = runSshScript({ - sshCmd, - sshHost, - sshPort, - sshUser, - sshKeyPath, + sshCmd, sshHost, sshPort, sshUser, sshKeyPath, script: 'echo vm-ready', inheritOutput: false }); @@ -1197,7 +941,6 @@ function waitForVmSshReady(options) { } sleepMs(2000); } - fail(`Timed out waiting for VM SSH readiness after ${timeoutSeconds}s.`); } @@ -1213,17 +956,6 @@ function resolveVmSshTimeoutSeconds(env) { return parsed; } -function resolveVmToolRunner(tool, adapter, env) { - const overrideKey = `PCODER_VM_${tool.toUpperCase()}_CMD`; - if (env[overrideKey]) { - return env[overrideKey]; - } - if (adapter.candidate_commands && adapter.candidate_commands.length > 0) { - return adapter.candidate_commands[0]; - } - return tool; -} - function buildRemoteProjectPath(remoteRoot, projectPath) { const normalizedRoot = remoteRoot.endsWith('/') ? remoteRoot.slice(0, -1) : remoteRoot; const baseRaw = path.basename(projectPath) || 'project'; @@ -1233,20 +965,9 @@ function buildRemoteProjectPath(remoteRoot, projectPath) { } function buildRemoteRunScript(options) { - const { - tool, - authMode, - remoteProjectPath, - vmRunner, - toolArgs, - mergedEnv - } = options; + const { authMode, remoteProjectPath, toolArgs, mergedEnv } = options; const forwardKeys = [ - 'OPENAI_API_KEY', - 'OPENAI_BASE_URL', - 'OPENAI_ORG_ID', - 'OPENAI_PROJECT', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', @@ -1274,7 +995,7 @@ function buildRemoteRunScript(options) { ]; if ((authMode || 'oauth') === 'oauth') { - const vmAuthHome = mergedEnv.PCODER_VM_AUTH_HOME || `/home/portable/.pcoder-auth/${tool}`; + const vmAuthHome = mergedEnv.PCODER_VM_AUTH_HOME || '/home/portable/.pcoder-auth/claude'; const vmConfig = `${vmAuthHome}/.config`; const vmCache = `${vmAuthHome}/.cache`; const vmData = `${vmAuthHome}/.local/share`; @@ -1285,14 +1006,8 @@ function buildRemoteRunScript(options) { lines.push(`export XDG_CACHE_HOME=${shellEscape(vmCache)}`); lines.push(`export XDG_DATA_HOME=${shellEscape(vmData)}`); lines.push(`export XDG_STATE_HOME=${shellEscape(vmState)}`); - if (tool === 'claude') { - lines.push(`export CLAUDE_CONFIG_DIR=${shellEscape(`${vmAuthHome}/.claude`)}`); - lines.push(`mkdir -p ${shellEscape(`${vmAuthHome}/.claude`)}`); - } - if (tool === 'codex') { - lines.push(`export OPENAI_HOME=${shellEscape(`${vmAuthHome}/.openai`)}`); - lines.push(`mkdir -p ${shellEscape(`${vmAuthHome}/.openai`)}`); - } + lines.push(`export CLAUDE_CONFIG_DIR=${shellEscape(`${vmAuthHome}/.claude`)}`); + lines.push(`mkdir -p ${shellEscape(`${vmAuthHome}/.claude`)}`); } lines.push(`export PCODER_AUTH_MODE=${shellEscape(authMode || 'oauth')}`); @@ -1303,38 +1018,22 @@ function buildRemoteRunScript(options) { } } - const cmdParts = [vmRunner, ...toolArgs].map((part) => shellEscape(part)); + const cmdParts = ['claude', ...toolArgs].map((part) => shellEscape(part)); lines.push(cmdParts.join(' ')); return lines.join('\n'); } function syncProjectToVm(options) { - const { - scpCmd, - sshHost, - sshPort, - sshUser, - sshKeyPath, - projectPath, - remoteProjectPath - } = options; - + const { scpCmd, sshHost, sshPort, sshUser, sshKeyPath, projectPath, remoteProjectPath } = options; const args = [ '-P', String(sshPort), '-i', sshKeyPath, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=no', '-o', `UserKnownHostsFile=${knownHostsNullPath()}`, - '-r', - '.', - `${sshUser}@${sshHost}:${remoteProjectPath}` + '-r', '.', `${sshUser}@${sshHost}:${remoteProjectPath}` ]; - - const result = cp.spawnSync(scpCmd, args, { - cwd: projectPath, - stdio: 'inherit' - }); - + const result = cp.spawnSync(scpCmd, args, { cwd: projectPath, stdio: 'inherit' }); if (result.error) { fail(`Failed to sync project into VM: ${result.error.message}`); } @@ -1344,32 +1043,16 @@ function syncProjectToVm(options) { } function syncProjectFromVm(options) { - const { - scpCmd, - sshHost, - sshPort, - sshUser, - sshKeyPath, - projectPath, - remoteProjectPath - } = options; - + const { scpCmd, sshHost, sshPort, sshUser, sshKeyPath, projectPath, remoteProjectPath } = options; const args = [ '-P', String(sshPort), '-i', sshKeyPath, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=no', '-o', `UserKnownHostsFile=${knownHostsNullPath()}`, - '-r', - `${sshUser}@${sshHost}:${remoteProjectPath}/.`, - '.' + '-r', `${sshUser}@${sshHost}:${remoteProjectPath}/.`, '.' ]; - - const result = cp.spawnSync(scpCmd, args, { - cwd: projectPath, - stdio: 'inherit' - }); - + const result = cp.spawnSync(scpCmd, args, { cwd: projectPath, stdio: 'inherit' }); if (result.error) { fail(`Failed to sync project back from VM: ${result.error.message}`); } @@ -1379,16 +1062,7 @@ function syncProjectFromVm(options) { } function runSshScript(options) { - const { - sshCmd, - sshHost, - sshPort, - sshUser, - sshKeyPath, - script, - inheritOutput - } = options; - + const { sshCmd, sshHost, sshPort, sshUser, sshKeyPath, script, inheritOutput } = options; const args = [ '-p', String(sshPort), '-i', sshKeyPath, @@ -1399,13 +1073,11 @@ function runSshScript(options) { `${sshUser}@${sshHost}`, 'bash', '-s' ]; - const result = cp.spawnSync(sshCmd, args, { input: script, encoding: 'utf8', stdio: inheritOutput ? ['pipe', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe'] }); - if (result.error) { fail(`SSH command failed to start: ${result.error.message}`); } @@ -1432,35 +1104,12 @@ function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } -function resolveRunner(adapter, env) { - const overrideKey = adapter.command_env; - if (overrideKey && env[overrideKey]) { - return env[overrideKey]; - } - - for (const candidate of adapter.candidate_commands || []) { - if (commandExists(candidate)) { - return candidate; - } - } - return null; -} - function commandExists(command) { const checker = process.platform === 'win32' ? 'where' : 'which'; - const result = cp.spawnSync(checker, [command], { - stdio: 'ignore' - }); + const result = cp.spawnSync(checker, [command], { stdio: 'ignore' }); return result.status === 0; } -function readActiveProfile() { - if (!fs.existsSync(activeProfilePath)) { - return ''; - } - return fs.readFileSync(activeProfilePath, 'utf8').trim(); -} - function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true });