From a23cc53ea94feebe4305937216546eada9d5a303 Mon Sep 17 00:00:00 2001 From: Ashutosh Tiwari Date: Mon, 29 Jun 2026 13:40:27 -0700 Subject: [PATCH 01/15] refactor(brand): rename Parallel Code -> Forge across code, paths, and strings - Identifiers: name/productName/appId (com.forge.app), StartupWMClass - Functional paths: .forge/, ~/.forge/agent-auth/, forge-* temp prefixes - MCP server key parallel-code -> forge; env vars PARALLEL_CODE_* -> FORGE_* - Code symbols ParallelCode* -> Forge*; updated test fixtures + a stale length assertion - Technical word 'parallel' (in parallel/parallelization) left untouched --- .gitignore | 2 +- .gitleaks.toml | 10 +- .prettierignore | 2 +- CLAUDE.md | 2 +- PRIVACY.md | 74 +++++----- README.md | 24 ++-- build/logo-preview.html | 2 +- build/logo-text-squared.svg | 2 +- build/logo-text.svg | 2 +- docker/Dockerfile | 4 +- electron/ipc/docker-config.test.ts | 34 ++--- electron/ipc/git-symlink-excludes.test.ts | 6 +- electron/ipc/git.ts | 4 +- electron/ipc/pty.test.ts | 74 +++++----- electron/ipc/pty.ts | 22 +-- electron/ipc/register-mcp.test.ts | 34 ++--- electron/ipc/register.ts | 45 +++--- electron/mcp/agent-args.test.ts | 6 +- electron/mcp/agent-args.ts | 14 +- electron/mcp/agent-frame-fixtures.ts | 20 ++- electron/mcp/atomic.ts | 4 +- electron/mcp/config.test.ts | 24 ++-- electron/mcp/config.ts | 4 +- ...oordinator-real-agents.integration.test.ts | 12 +- .../coordinator-real-pty.integration.test.ts | 6 +- electron/mcp/coordinator.test.ts | 136 +++++++++--------- electron/mcp/coordinator.ts | 40 +++--- electron/mcp/docker.integration.test.ts | 118 ++++++++------- electron/mcp/mcp-tool-list.ts | 2 +- electron/mcp/preamble.ts | 4 +- electron/mcp/prompt-detect.test.ts | 12 +- electron/mcp/server.ts | 11 +- electron/mcp/sub-task-preamble.ts | 4 +- electron/mcp/types.ts | 10 +- electron/vite.config.electron.ts | 2 +- index.html | 2 +- package-lock.json | 4 +- package.json | 12 +- scripts/check-coordinator-run.mjs | 2 +- scripts/fake-agent.mjs | 2 +- scripts/test-semgrep-filesystem-safety.mjs | 2 +- src/App.tsx | 4 +- src/components/CloseTaskDialog.tsx | 2 +- src/components/ImportWorktreesDialog.tsx | 5 +- src/components/Sidebar.tsx | 2 +- .../prompt-autosend-readiness.test.ts | 8 +- src/lib/agent-args.test.ts | 8 +- src/lib/custom-theme.ts | 2 +- src/lib/docker.test.ts | 2 +- src/lib/docker.ts | 6 +- src/lib/terminalDrop.test.ts | 4 +- src/remote/AgentList.tsx | 6 +- src/remote/App.tsx | 2 +- src/remote/auth.ts | 2 +- src/remote/index.html | 2 +- src/store/coordinator-preamble.ts | 4 +- src/store/core.ts | 2 +- src/store/persistence.ts | 4 +- src/store/tasks.test.ts | 2 +- src/store/tasks.ts | 6 +- src/store/ui.ts | 2 +- 61 files changed, 421 insertions(+), 450 deletions(-) diff --git a/.gitignore b/.gitignore index 30c4bd74..69b9769e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ coverage # Local auto-update test server payload (build artifacts, not source). update-test/ # Runtime data dir written by the app (MCP coordinator state, etc.) -.parallel-code/ +.forge/ .worktrees .idea .claude diff --git a/.gitleaks.toml b/.gitleaks.toml index 3ae13382..25419f49 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,14 +1,14 @@ -title = "Gitleaks config for Parallel Code" +title = "Gitleaks config for Forge" [extend] # Use the default Gitleaks ruleset as the base useDefault = true [[rules]] -id = "parallel-code-mcp-token" -description = "Parallel Code MCP bearer token" -regex = '''PARALLEL_CODE_MCP_TOKEN\s*[=:]\s*['"]?[A-Za-z0-9+/_-]{20,}['"]?''' -tags = ["token", "parallel-code"] +id = "forge-mcp-token" +description = "Forge MCP bearer token" +regex = '''FORGE_MCP_TOKEN\s*[=:]\s*['"]?[A-Za-z0-9+/_-]{20,}['"]?''' +tags = ["token", "forge"] [[rules]] id = "bearer-token-in-url" diff --git a/.prettierignore b/.prettierignore index 12dffc98..7ba78cf6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,7 @@ node_modules/ .worktrees/ .claude/ .remember/ -.parallel-code/ +.forge/ .enzyme/ .omc/ .planning/ diff --git a/CLAUDE.md b/CLAUDE.md index a088e724..697febaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Parallel Code +# Forge Electron desktop app — SolidJS frontend, Node.js backend. Published for **macOS and Linux only** (no Windows). diff --git a/PRIVACY.md b/PRIVACY.md index 4226f663..24b4986a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -2,11 +2,11 @@ _Last updated: 2026-05-26_ -Parallel Code is an open-source desktop application that runs entirely on your local machine. This policy describes what data the application handles and how. +Forge is an open-source desktop application that runs entirely on your local machine. This policy describes what data the application handles and how. ## Summary -**The Parallel Code project operates no remote servers and does not collect, transmit, or store your data on any infrastructure we control.** +**The Forge project operates no remote servers and does not collect, transmit, or store your data on any infrastructure we control.** - No analytics - No telemetry @@ -16,9 +16,9 @@ Parallel Code is an open-source desktop application that runs entirely on your l - No remote logging - No advertising or third-party trackers -## What data Parallel Code handles +## What data Forge handles -Except where explicitly disclosed in the "Network activity initiated by Parallel Code itself" section below, data Parallel Code handles stays on your computer: +Except where explicitly disclosed in the "Network activity initiated by Forge itself" section below, data Forge handles stays on your computer: - **Your source code, git repositories, and worktrees** — read and written locally on your filesystem. - **Task metadata, notes, prompts, settings, and UI state** — stored locally in the application's data directory. @@ -28,7 +28,7 @@ Except where explicitly disclosed in the "Network activity initiated by Parallel ## Third-party AI coding CLIs -Parallel Code is a local interface for third-party AI coding CLIs and agent commands that you install and configure yourself, including: +Forge is a local interface for third-party AI coding CLIs and agent commands that you install and configure yourself, including: - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (Anthropic) - [Codex CLI](https://github.com/openai/codex) (OpenAI) @@ -37,78 +37,78 @@ Parallel Code is a local interface for third-party AI coding CLIs and agent comm - [OpenCode](https://opencode.ai/) (a multi-provider AI coding agent) - Arena presets such as Aider and any custom agent commands you add yourself -When you dispatch an agent, Parallel Code starts the selected CLI or command as a local subprocess on your machine, or inside a local Docker container when Docker mode is enabled. **That tool may then communicate with whatever vendor, model provider, repository host, package registry, or other service it is configured to use, under that service's own privacy policy and terms.** Parallel Code is not a party to those network communications and does not proxy them through infrastructure operated by the Parallel Code project. +When you dispatch an agent, Forge starts the selected CLI or command as a local subprocess on your machine, or inside a local Docker container when Docker mode is enabled. **That tool may then communicate with whatever vendor, model provider, repository host, package registry, or other service it is configured to use, under that service's own privacy policy and terms.** Forge is not a party to those network communications and does not proxy them through infrastructure operated by the Forge project. **You are responsible for reviewing the privacy policy and terms of each third-party tool you choose to use.** Data sent to those services — including prompts, source code, and conversation history — is governed by their policies, not this one. -For sub-tasks run under a coordinator, Parallel Code prepends a short Parallel-Code-authored system-prompt preamble to the agent's first turn (for example, instructing the sub-task to call the `signal_done` MCP tool when finished). For Claude Code specifically, this preamble is injected by writing an entry into `.claude/settings.local.json` in the worktree; other agents receive the preamble through their own mechanism. The CLI then sends that preamble to its vendor alongside your prompt. +For sub-tasks run under a coordinator, Forge prepends a short Forge-authored system-prompt preamble to the agent's first turn (for example, instructing the sub-task to call the `signal_done` MCP tool when finished). For Claude Code specifically, this preamble is injected by writing an entry into `.claude/settings.local.json` in the worktree; other agents receive the preamble through their own mechanism. The CLI then sends that preamble to its vendor alongside your prompt. -## Network activity initiated by Parallel Code itself +## Network activity initiated by Forge itself -Unlike the AI CLIs above, the following network activity is initiated by Parallel Code itself, either directly or by invoking local tooling on your machine: +Unlike the AI CLIs above, the following network activity is initiated by Forge itself, either directly or by invoking local tooling on your machine: -- **Application updates** — on packaged macOS and Linux AppImage builds, Parallel Code asks GitHub Releases whether a newer version exists about ten seconds after launch (and again if you click "Check for updates" in the UI). The request goes to the public GitHub Releases endpoint. In addition to your IP address, the underlying update library ([`electron-updater`](https://www.electron.build/auto-update)) sends two pieces of metadata that GitHub can see: a `User-Agent` header with the literal value `electron-builder`, and an `x-user-staging-id` header containing a random UUID generated locally on first launch and stored as `.updaterId` in the app's data directory. **This is a stable per-install identifier sent to GitHub on every update check** — GitHub can correlate the timing, frequency, and IP of those checks per install under its own policies. The Parallel Code project itself does not receive or store it. If you want a fresh identifier, delete `.updaterId`; the next check will generate a new UUID. Downloading an update only happens when you confirm it; once downloaded, the update is applied automatically the next time you quit the app. -- **GitHub PR status** — when a task is linked to a GitHub pull request, Parallel Code invokes your locally installed `gh` CLI to fetch the PR's state and check status. While the app window is visible, pending PRs are polled roughly every 30 seconds and already-settled PRs every five minutes. The `gh` CLI then contacts GitHub under your own existing authentication. +- **Application updates** — on packaged macOS and Linux AppImage builds, Forge asks GitHub Releases whether a newer version exists about ten seconds after launch (and again if you click "Check for updates" in the UI). The request goes to the public GitHub Releases endpoint. In addition to your IP address, the underlying update library ([`electron-updater`](https://www.electron.build/auto-update)) sends two pieces of metadata that GitHub can see: a `User-Agent` header with the literal value `electron-builder`, and an `x-user-staging-id` header containing a random UUID generated locally on first launch and stored as `.updaterId` in the app's data directory. **This is a stable per-install identifier sent to GitHub on every update check** — GitHub can correlate the timing, frequency, and IP of those checks per install under its own policies. The Forge project itself does not receive or store it. If you want a fresh identifier, delete `.updaterId`; the next check will generate a new UUID. Downloading an update only happens when you confirm it; once downloaded, the update is applied automatically the next time you quit the app. +- **GitHub PR status** — when a task is linked to a GitHub pull request, Forge invokes your locally installed `gh` CLI to fetch the PR's state and check status. While the app window is visible, pending PRs are polled roughly every 30 seconds and already-settled PRs every five minutes. The `gh` CLI then contacts GitHub under your own existing authentication. - **Other git operations** — `push`, `pull`, and `fetch` run only when you explicitly invoke them, using your local `git` tooling and its credentials. One implicit exception: when the app needs to know your repository's default branch and the cached remote-tracking ref is stale, it runs `git remote set-head origin --auto`, which contacts your configured remote. -- **Docker image builds** — when you click "Build Image" for Docker mode, Parallel Code invokes your local `docker` CLI / Docker daemon to run `docker build`. The bundled Dockerfile can contact Docker registries, Ubuntu package mirrors, NodeSource, GitHub CLI package repositories, and npm while building the image. If your project contains `.parallel-code/Dockerfile`, Parallel Code can build that Dockerfile instead using the project root as the Docker build context; if your Docker CLI is configured to use a remote Docker daemon or remote context, Docker may send that build context to that daemon according to your Docker configuration. -- **Inline code Q&A** — by default, the inline Q&A feature uses the Claude Code CLI as a local subprocess. The prompt sent to that CLI includes your question, a fixed instruction, and the selection you asked about — either a code snippet with file path and line range, or a markdown snippet from the in-app plan viewer with its source heading and an approximate position within the document. When you enable the optional [MiniMax](https://www.minimax.io/) provider in Settings, MiniMax is called directly by Parallel Code rather than as a subprocess: Parallel Code itself makes an HTTPS request to `api.minimax.io` using your API key, with the same kind of selected context in the request body. Your MiniMax API key is held in the main process in memory only and is not written to disk. Data sent to MiniMax is governed by MiniMax's own privacy policy, not this one. -- **Remote Access (mobile monitoring)** — when you start it from Settings, Parallel Code runs a local HTTP and WebSocket listener bound to all of your machine's network interfaces, so devices on your LAN — or any device that can reach an address shown by the app — can connect. No traffic is routed through infrastructure operated by the Parallel Code project. +- **Docker image builds** — when you click "Build Image" for Docker mode, Forge invokes your local `docker` CLI / Docker daemon to run `docker build`. The bundled Dockerfile can contact Docker registries, Ubuntu package mirrors, NodeSource, GitHub CLI package repositories, and npm while building the image. If your project contains `.forge/Dockerfile`, Forge can build that Dockerfile instead using the project root as the Docker build context; if your Docker CLI is configured to use a remote Docker daemon or remote context, Docker may send that build context to that daemon according to your Docker configuration. +- **Inline code Q&A** — by default, the inline Q&A feature uses the Claude Code CLI as a local subprocess. The prompt sent to that CLI includes your question, a fixed instruction, and the selection you asked about — either a code snippet with file path and line range, or a markdown snippet from the in-app plan viewer with its source heading and an approximate position within the document. When you enable the optional [MiniMax](https://www.minimax.io/) provider in Settings, MiniMax is called directly by Forge rather than as a subprocess: Forge itself makes an HTTPS request to `api.minimax.io` using your API key, with the same kind of selected context in the request body. Your MiniMax API key is held in the main process in memory only and is not written to disk. Data sent to MiniMax is governed by MiniMax's own privacy policy, not this one. +- **Remote Access (mobile monitoring)** — when you start it from Settings, Forge runs a local HTTP and WebSocket listener bound to all of your machine's network interfaces, so devices on your LAN — or any device that can reach an address shown by the app — can connect. No traffic is routed through infrastructure operated by the Forge project. - **Transport.** Traffic is unencrypted HTTP, and the access token is included in the URL (e.g. in the QR code). **Treat that URL as a credential.** Anything that captures the URL has the token until you restart Remote Access — including a photo of the QR code (which may be auto-backed up by your phone), mobile browser history or cross-device sync, clipboard managers, screen-sharing or screen-recording tools, and corporate TLS-inspection proxies or appliances that log HTTP URLs. Treat the network it runs on as trusted (a private LAN or a Tailscale tailnet), and stop the server when you're done. - **Tokens.** A fresh bearer token is generated each time the server starts and is not persisted by the desktop app; previous tokens become invalid on restart. The mobile client stores the token it receives in its browser `localStorage` so the same device can reconnect without rescanning; the token persists there in plaintext until you clear browser data on the device or restart Remote Access. - **Capabilities.** An authenticated mobile client is **read-only** — it can see the list of running agents and their recent terminal output, but cannot send input or stop agents. - - **"Tailscale-like" detection.** Parallel Code labels an interface as Tailscale-like when it finds a non-internal IPv4 address beginning with `100.`; addresses starting with `172.` (often used by Docker bridges) are excluded from the displayed "WiFi" URL. The check is heuristic and does not verify the interface is actually Tailscale, so only treat that option as Tailscale if you know the address belongs to your tailnet. If your host has a VPN, virtualisation bridge, or unusual NIC, the URL shown may be reachable from a wider network than your LAN. + - **"Tailscale-like" detection.** Forge labels an interface as Tailscale-like when it finds a non-internal IPv4 address beginning with `100.`; addresses starting with `172.` (often used by Docker bridges) are excluded from the displayed "WiFi" URL. The check is heuristic and does not verify the interface is actually Tailscale, so only treat that option as Tailscale if you know the address belongs to your tailnet. If your host has a VPN, virtualisation bridge, or unusual NIC, the URL shown may be reachable from a wider network than your LAN. - **Over Tailscale.** Traffic is carried by your tailnet — typically a direct WireGuard connection between your devices, but Tailscale's coordination service and (when direct connection is not possible) DERP relays may be involved per [Tailscale's network architecture](https://tailscale.com/kb/1257/connection-types). How Tailscale handles that traffic is governed by Tailscale's own policies, not this one. -- **Sub-task coordinator (MCP)** — when sub-tasks run under a coordinator agent, Parallel Code starts a local token-protected HTTP/WebSocket server so sub-task agents can call back into the app (e.g. to signal completion). No traffic from this feature passes through infrastructure operated by the Parallel Code project. +- **Sub-task coordinator (MCP)** — when sub-tasks run under a coordinator agent, Forge starts a local token-protected HTTP/WebSocket server so sub-task agents can call back into the app (e.g. to signal completion). No traffic from this feature passes through infrastructure operated by the Forge project. - **Bind address.** When MCP starts its own listener, it binds to `127.0.0.1` except on macOS Docker setups, where it binds to `0.0.0.0` so containers can reach it via `host.docker.internal` — this also makes the port reachable from other hosts on your LAN, though access still requires the token. If a Remote Access server is already running when a coordinator starts, the coordinator reuses that listener; because Remote Access binds to `0.0.0.0`, MCP routes inherit that LAN reach on any platform (including Linux), though access still requires the MCP token. - - **Where the token can land.** Token-bearing MCP data is written or passed in several places: a worktree `.mcp.json` when a worktree path is available, or a project-root `.mcp.json` otherwise, so the coordinator agent can auto-discover the server (Parallel Code also adds `.mcp.json` to your `.git/info/exclude` so it is not committed); a non-Docker coordinator config in your OS temp directory named `parallel-code-mcp-.json`; per-sub-task configs in your OS temp directory for host-mode sub-tasks (`parallel-code-subtask-.json`) or under the coordinator's `.parallel-code/` directory for Docker sub-tasks (`subtask-.json`); and short-lived `.parallel-code-atomic-.tmp` files written next to these configs during atomic-rename steps. These files are written with `0600` permissions where the platform supports it. - - **Codex token in the command line.** For Codex specifically, the MCP token is passed as a literal command-line argument (`--config mcp_servers.parallel-code={... env = { PARALLEL_CODE_MCP_TOKEN = "..." }}`). Process command lines are visible to other processes — via `/proc//cmdline` on Linux or `ps` on macOS — so any local process that runs concurrently with a Codex sub-task can read that token and call back into the coordinator under its authority until the coordinator exits. Other agents receive the token through a token-protected file or env var instead. + - **Where the token can land.** Token-bearing MCP data is written or passed in several places: a worktree `.mcp.json` when a worktree path is available, or a project-root `.mcp.json` otherwise, so the coordinator agent can auto-discover the server (Forge also adds `.mcp.json` to your `.git/info/exclude` so it is not committed); a non-Docker coordinator config in your OS temp directory named `forge-mcp-.json`; per-sub-task configs in your OS temp directory for host-mode sub-tasks (`forge-subtask-.json`) or under the coordinator's `.forge/` directory for Docker sub-tasks (`subtask-.json`); and short-lived `.forge-atomic-.tmp` files written next to these configs during atomic-rename steps. These files are written with `0600` permissions where the platform supports it. + - **Codex token in the command line.** For Codex specifically, the MCP token is passed as a literal command-line argument (`--config mcp_servers.forge={... env = { FORGE_MCP_TOKEN = "..." }}`). Process command lines are visible to other processes — via `/proc//cmdline` on Linux or `ps` on macOS — so any local process that runs concurrently with a Codex sub-task can read that token and call back into the coordinator under its authority until the coordinator exits. Other agents receive the token through a token-protected file or env var instead. - **Docker task isolation** — when you enable Docker mode for a task, or when you opt coordinator sub-tasks into Docker-isolated mode, the agent launches in a container via `docker run --network host`. **Docker mode is not a security boundary.** It isolates the filesystem against the worktree, but does not isolate the network or credentials from the agent. - **Network.** `--network host` means the container shares the host's network namespace; its outbound reachability is the same as your host's, including loopback services and any LAN address your host can reach. - - **Environment.** Most of your shell environment is forwarded into the container with `-e`. A blocklist strips host-specific variables — `PATH`, `HOME`, all temp-dir vars (`TMPDIR`/`TEMPDIR`/`TMP`/`TEMP`), display/desktop session vars (`DISPLAY`, `WAYLAND_DISPLAY`, `DBUS_*`, `XAUTHORITY`), linker overrides (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `DYLD_*`), session vars (`LOGNAME`, `MAIL`), agent socket / k8s vars (`SSH_AUTH_SOCK`, `GPG_AGENT_INFO`, `KUBECONFIG`), and the `XDG_*`/`ELECTRON_*`/`SUDO_*`/`CLAUDE_CODE_*` families (the authoritative list is `DOCKER_ENV_BLOCK_LIST` in `electron/ipc/pty.ts`). **API keys and tokens in your shell are not on that blocklist** — for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GITHUB_TOKEN`/`GH_TOKEN`, `HF_TOKEN`, or AWS credentials are forwarded into the container by default. Unset anything you do not want reaching the containerised agent before launching Parallel Code. + - **Environment.** Most of your shell environment is forwarded into the container with `-e`. A blocklist strips host-specific variables — `PATH`, `HOME`, all temp-dir vars (`TMPDIR`/`TEMPDIR`/`TMP`/`TEMP`), display/desktop session vars (`DISPLAY`, `WAYLAND_DISPLAY`, `DBUS_*`, `XAUTHORITY`), linker overrides (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `DYLD_*`), session vars (`LOGNAME`, `MAIL`), agent socket / k8s vars (`SSH_AUTH_SOCK`, `GPG_AGENT_INFO`, `KUBECONFIG`), and the `XDG_*`/`ELECTRON_*`/`SUDO_*`/`CLAUDE_CODE_*` families (the authoritative list is `DOCKER_ENV_BLOCK_LIST` in `electron/ipc/pty.ts`). **API keys and tokens in your shell are not on that blocklist** — for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GITHUB_TOKEN`/`GH_TOKEN`, `HF_TOKEN`, or AWS credentials are forwarded into the container by default. Unset anything you do not want reaching the containerised agent before launching Forge. - **Worktree mount.** The container bind-mounts the worktree (and its parent for coordinator tasks) plus the main repo's `.git` directory. - **Read-only credential mounts.** To allow the agent to authenticate to git/GitHub/npm/etc., the following host files are bind-mounted **read-only** into the container's home if they exist: `~/.ssh`, `~/.gitconfig`, `~/.config/gh`, `~/.npmrc`, `~/.netrc`, and the file pointed to by `$GOOGLE_APPLICATION_CREDENTIALS`. Read-only stops modification, not reading — the agent can still read and use any credential mounted this way. - - **Writable shared agent auth (opt-in).** If you enable "Share agent auth across Linux containers" in Settings, Parallel Code creates and bind-mounts **writable** host directories under `~/.parallel-code/agent-auth//` into the container's per-agent config locations (e.g. `~/.claude`, `~/.codex`, `~/.gemini`, `~/.config/opencode`, `~/.config/github-copilot`, and `~/.claude.json`), so the containerised agent can write fresh credentials on first login and reuse them on later runs. Those host directories then accumulate agent credentials in your home directory across sessions. For Claude specifically, Parallel Code also seeds `hasTrustDialogAccepted: true` and `hasCompletedProjectOnboarding: true` for the worktree path inside the mounted `.claude.json`, so the agent does not prompt for trust on first run in a freshly mounted container. + - **Writable shared agent auth (opt-in).** If you enable "Share agent auth across Linux containers" in Settings, Forge creates and bind-mounts **writable** host directories under `~/.forge/agent-auth//` into the container's per-agent config locations (e.g. `~/.claude`, `~/.codex`, `~/.gemini`, `~/.config/opencode`, `~/.config/github-copilot`, and `~/.claude.json`), so the containerised agent can write fresh credentials on first login and reuse them on later runs. Those host directories then accumulate agent credentials in your home directory across sessions. For Claude specifically, Forge also seeds `hasTrustDialogAccepted: true` and `hasCompletedProjectOnboarding: true` for the worktree path inside the mounted `.claude.json`, so the agent does not prompt for trust on first run in a freshly mounted container. -Across all of the above, no traffic is routed through servers operated by the Parallel Code project. Most of it does reach third parties — GitHub, Docker registries, MiniMax, Anthropic, OpenAI, your tailnet, or whatever your chosen AI CLI talks to — whose own privacy policies govern what they receive. +Across all of the above, no traffic is routed through servers operated by the Forge project. Most of it does reach third parties — GitHub, Docker registries, MiniMax, Anthropic, OpenAI, your tailnet, or whatever your chosen AI CLI talks to — whose own privacy policies govern what they receive. ## OS-level integrations A few features hand short pieces of data to your operating system itself: -- **Clipboard pasting and drag-drop** — when you paste into or drop content onto an agent prompt, Parallel Code reads the OS clipboard or the drop payload. If it contains a file reference (`public.file-url`, `text/uri-list`, or GNOME's `x-special/gnome-copied-files`), the file path is passed to the agent. If it contains an image, the image bytes are written to your OS temp directory so the agent can reference the image as a file path: clipboard pastes overwrite a single file at `$TMPDIR/parallel-code-clipboard.png`, and drops are written to `$TMPDIR/parallel-code-drop--[-]` — the original drop filename, if any, is sanitised and appended. These temp files are not deleted by Parallel Code; they remain in your temp directory until your OS cleans it (typically on reboot or via periodic cleanup). Plain text is inserted into the prompt. As with any prompt, once you submit it (or the app detects it as prompt input from the terminal), it can be stored locally as task metadata (the task's `lastPrompt`) in `state.json`. -- **Clipboard writes** — when you click "Copy" in certain UI controls (for example the Connect Phone dialog), Parallel Code writes to the OS clipboard. The Remote Access URL it copies contains the **session bearer token** as a query parameter, so anything that reads or syncs your clipboard (clipboard managers, screen-sharing tools, cross-device clipboard sync) will see that token until you copy something else or restart Remote Access. Other clipboard writes (terminal selection, theme prompts, task steps) carry only the content you asked to copy. -- **Native notifications** — when a task completes, needs attention, or when GitHub PR checks succeed or fail, Parallel Code uses the OS notification API. The notification's title and body (typically the task name and a short status; for failed PR checks, the failed check names) are visible to the operating system and any surfaces that mirror notifications (Notification Center, etc.). +- **Clipboard pasting and drag-drop** — when you paste into or drop content onto an agent prompt, Forge reads the OS clipboard or the drop payload. If it contains a file reference (`public.file-url`, `text/uri-list`, or GNOME's `x-special/gnome-copied-files`), the file path is passed to the agent. If it contains an image, the image bytes are written to your OS temp directory so the agent can reference the image as a file path: clipboard pastes overwrite a single file at `$TMPDIR/forge-clipboard.png`, and drops are written to `$TMPDIR/forge-drop--[-]` — the original drop filename, if any, is sanitised and appended. These temp files are not deleted by Forge; they remain in your temp directory until your OS cleans it (typically on reboot or via periodic cleanup). Plain text is inserted into the prompt. As with any prompt, once you submit it (or the app detects it as prompt input from the terminal), it can be stored locally as task metadata (the task's `lastPrompt`) in `state.json`. +- **Clipboard writes** — when you click "Copy" in certain UI controls (for example the Connect Phone dialog), Forge writes to the OS clipboard. The Remote Access URL it copies contains the **session bearer token** as a query parameter, so anything that reads or syncs your clipboard (clipboard managers, screen-sharing tools, cross-device clipboard sync) will see that token until you copy something else or restart Remote Access. Other clipboard writes (terminal selection, theme prompts, task steps) carry only the content you asked to copy. +- **Native notifications** — when a task completes, needs attention, or when GitHub PR checks succeed or fail, Forge uses the OS notification API. The notification's title and body (typically the task name and a short status; for failed PR checks, the failed check names) are visible to the operating system and any surfaces that mirror notifications (Notification Center, etc.). - **Microphone entitlement (macOS)** — the packaged macOS app declares `NSMicrophoneUsageDescription` and the corresponding hardened-runtime entitlement, so the OS will permit microphone access if a feature requests it. The current build contains no active microphone capture code; granting the permission has no effect until a microphone-using feature ships. Note that the renderer permission handler in `electron/main.ts` auto-approves audio media requests — so if microphone-using code is added later, the only consent gate will be the OS-level microphone prompt. -- **Rendered markdown** — plan content shown in the task notes panel and the plan viewer dialog, plus any `.md` files you open from the UI, are rendered to HTML by the app for display. The renderer sanitises the output but allows standard image (``) and link (``) tags. If markdown rendered in the UI contains an external `` reference — for example because an agent wrote one into a plan or markdown file — your renderer will fetch that resource directly from the third-party host as soon as the content is shown, leaking your IP and the resource URL to that host. Parallel Code does not insert such references itself. +- **Rendered markdown** — plan content shown in the task notes panel and the plan viewer dialog, plus any `.md` files you open from the UI, are rendered to HTML by the app for display. The renderer sanitises the output but allows standard image (``) and link (``) tags. If markdown rendered in the UI contains an external `` reference — for example because an agent wrote one into a plan or markdown file — your renderer will fetch that resource directly from the third-party host as soon as the content is shown, leaking your IP and the resource URL to that host. Forge does not insert such references itself. ## Local storage locations -Configuration and state are kept in standard per-OS application directories — on macOS under `~/Library/Application Support/Parallel Code`, on Linux under `~/.config/Parallel Code`. Files in those directories include: +Configuration and state are kept in standard per-OS application directories — on macOS under `~/Library/Application Support/Forge`, on Linux under `~/.config/Forge`. Files in those directories include: - `state.json` and a rolling `state.json.bak` (overwritten on the next save) so a corrupted write does not lose your tasks. - `keybindings.json` and `keybindings.json.bak` for custom keybindings. - `themes/` — custom theme CSS files. - `arena-presets.json` and `arena-history.json` for the Arena feature. - `.updaterId` — the per-install UUID sent in the `x-user-staging-id` header on update checks (see Application updates). -- Chromium's standard cache and storage directories (`Cache/`, `Code Cache/`, `GPUCache/`, `Local Storage/`, `IndexedDB/`, `Network/`, and similar) are also created inside the userData directory at first launch. Parallel Code's renderer does not use `localStorage`, `IndexedDB`, cookies, or service workers, so these directories hold only Chromium's own asset caches — not application state or user content. +- Chromium's standard cache and storage directories (`Cache/`, `Code Cache/`, `GPUCache/`, `Local Storage/`, `IndexedDB/`, `Network/`, and similar) are also created inside the userData directory at first launch. Forge's renderer does not use `localStorage`, `IndexedDB`, cookies, or service workers, so these directories hold only Chromium's own asset caches — not application state or user content. -Inside the git repositories you point the app at, Parallel Code may also write: +Inside the git repositories you point the app at, Forge may also write: -- `.claude/` — for Claude Code, Parallel Code seeds empty `settings.json` and `settings.local.json` placeholders on every spawn (so the bwrap sandbox can bind-mount them) and holds `steps.json` for agent step tracking. When sub-tasks run, `.claude/settings.local.json` also carries the coordinator preamble described under _Third-party AI coding CLIs_ above, plus similar agent-scoped scratch state. -- `.parallel-code/` — when you use Docker mode or coordinator sub-tasks, this is created in the worktree for coordination state, project Docker configuration (e.g. an optional `.parallel-code/Dockerfile`), and per-sub-task MCP token configs. -- `.git/info/exclude` — Parallel Code appends `.mcp.json`, `.parallel-code/`, `.claude/steps.json`, and a small set of bwrap bind-mount artifact placeholders (`/.bashrc`, `/.gitconfig`, `/.zshrc`, and similar) so these app-managed paths do not surface in `git status`. +- `.claude/` — for Claude Code, Forge seeds empty `settings.json` and `settings.local.json` placeholders on every spawn (so the bwrap sandbox can bind-mount them) and holds `steps.json` for agent step tracking. When sub-tasks run, `.claude/settings.local.json` also carries the coordinator preamble described under _Third-party AI coding CLIs_ above, plus similar agent-scoped scratch state. +- `.forge/` — when you use Docker mode or coordinator sub-tasks, this is created in the worktree for coordination state, project Docker configuration (e.g. an optional `.forge/Dockerfile`), and per-sub-task MCP token configs. +- `.git/info/exclude` — Forge appends `.mcp.json`, `.forge/`, `.claude/steps.json`, and a small set of bwrap bind-mount artifact placeholders (`/.bashrc`, `/.gitconfig`, `/.zshrc`, and similar) so these app-managed paths do not surface in `git status`. -Other places Parallel Code can write state: +Other places Forge can write state: -- `~/.parallel-code/agent-auth//` — only if you enable "Share agent auth across Linux containers" (see Docker task isolation); accumulates agent credentials across sessions. -- OS temp directory (`$TMPDIR`) — `parallel-code-clipboard.png` and the `parallel-code-drop-*` files from clipboard/drop image handling (not deleted by Parallel Code), plus the host-mode coordinator and sub-task MCP configs and `.parallel-code-atomic-.tmp` files described under Sub-task coordinator above. +- `~/.forge/agent-auth//` — only if you enable "Share agent auth across Linux containers" (see Docker task isolation); accumulates agent credentials across sessions. +- OS temp directory (`$TMPDIR`) — `forge-clipboard.png` and the `forge-drop-*` files from clipboard/drop image handling (not deleted by Forge), plus the host-mode coordinator and sub-task MCP configs and `.forge-atomic-.tmp` files described under Sub-task coordinator above. Persistence the app itself does not control but that mirrors content the app produced: - The mobile client's bearer token persists in browser `localStorage` on the paired device until you clear browser data on that device or restart Remote Access (which invalidates the previous token). - Native notifications (task completion, PR check results) typically remain in the operating system's notification centre until you clear it. -Stop any running agents and coordinator sessions before deleting app-generated state so the app is not mid-write when files disappear. Also review `.parallel-code/` before deleting it, because it may contain project-owned configuration such as `.parallel-code/Dockerfile`, not only disposable coordination files. +Stop any running agents and coordinator sessions before deleting app-generated state so the app is not mid-write when files disappear. Also review `.forge/` before deleting it, because it may contain project-owned configuration such as `.forge/Dockerfile`, not only disposable coordination files. ## Your data and rights @@ -116,7 +116,7 @@ Because the project itself collects no personal data on infrastructure it contro ## Children's privacy -Parallel Code is a developer tool aimed at software developers, not children, and is not directed at users under 13. The project itself operates no servers and does not collect personal data on infrastructure it controls. The network requests Parallel Code does make (see _Network activity initiated by Parallel Code itself_ above) carry standard HTTP metadata such as your IP address to the destinations listed there; data those third parties receive is governed by their own privacy policies, not this one. +Forge is a developer tool aimed at software developers, not children, and is not directed at users under 13. The project itself operates no servers and does not collect personal data on infrastructure it controls. The network requests Forge does make (see _Network activity initiated by Forge itself_ above) carry standard HTTP metadata such as your IP address to the destinations listed there; data those third parties receive is governed by their own privacy policies, not this one. ## Changes to this policy @@ -124,6 +124,6 @@ If this policy changes, the update will appear in this file in the project repos ## Contact -Parallel Code is maintained by Johannes Millan, the same author as [Super Productivity](https://super-productivity.com) — the email below is that author's general contact address and reaches the same person. Questions or concerns can be raised as an issue on the [GitHub repository](https://github.com/johannesjo/parallel-code/issues) or by email at [contact@super-productivity.com](mailto:contact@super-productivity.com). GitHub issues are public — please do not include personal data, secrets (API keys, tokens), or anything you would not want indexed by search engines. If a question requires sharing personal data, use email rather than posting it publicly. +Forge is maintained by Johannes Millan, the same author as [Super Productivity](https://super-productivity.com) — the email below is that author's general contact address and reaches the same person. Questions or concerns can be raised as an issue on the [GitHub repository](https://github.com/johannesjo/forge/issues) or by email at [contact@super-productivity.com](mailto:contact@super-productivity.com). GitHub issues are public — please do not include personal data, secrets (API keys, tokens), or anything you would not want indexed by search engines. If a question requires sharing personal data, use email rather than posting it publicly. This policy is published alongside the project's open-source license: the project follows the behavior described above and will update this document when that behavior changes. The software itself is provided without warranty under the terms of the project's license. diff --git a/README.md b/README.md index be457922..6e9505a8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Parallel Code + Forge

@@ -22,7 +22,7 @@ SolidJS TypeScript macOS | Linux - License + License

@@ -32,7 +32,7 @@

- Parallel Code demo + Forge demo

## Screenshots @@ -43,7 +43,7 @@ | **Diff review with inline comments** | **AI Arena — race agents head-to-head** | | ![Diff review](screens/diff-dialog-code-comments.png) | ![AI Arena](screens/ai-arena-mode.png) | -## Why Parallel Code? +## Why Forge? - **Use the AI coding tools you already trust** — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) — all from one interface. - **Free and open source** — no extra subscription required. MIT licensed. @@ -67,7 +67,7 @@ ## How it works -When you create a task, Parallel Code: +When you create a task, Forge: 1. Creates a new git branch from your main branch 2. Sets up a [git worktree](https://git-scm.com/docs/git-worktree) so the agent works in a separate directory @@ -87,8 +87,8 @@ When you're happy with the result, merge the branch back to main from the sideba - **PR CI status watcher** — desktop notification when GitHub checks settle - Shell terminals per task, scoped to the worktree - **Direct mode** for working on the main branch without isolation, plus support for **folders without a git repo** -- **Existing worktree import** — bring already-created worktrees into Parallel Code -- **Sandboxing with project-specific Dockerfiles** — drop a `.parallel-code/Dockerfile` into the project and tasks run inside it +- **Existing worktree import** — bring already-created worktrees into Forge +- **Sandboxing with project-specific Dockerfiles** — drop a `.forge/Dockerfile` into the project and tasks run inside it - **Coverage radar** — per-file test-coverage badges in the Changed Files panel - **Configurable keyboard shortcuts** with per-agent presets - 10 themes — Islands Dark, Minimal, Graphite, Midnight, Classic, Indigo, Ember, Glacier, Zenburnesque, Workbench @@ -111,13 +111,13 @@ When you're happy with the result, merge the branch back to main from the sideba ## Getting Started -1. **Download** the latest release for your platform from the [releases page](https://github.com/johannesjo/parallel-code/releases/latest): +1. **Download** the latest release for your platform from the [releases page](https://github.com/johannesjo/forge/releases/latest): - **macOS** — `.dmg` (universal) - **Linux** — `.AppImage` or `.deb` 2. **Install at least one AI coding CLI:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Antigravity CLI](https://antigravity.google/), or [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) -3. **Open Parallel Code**, point it at a git repo, and start dispatching tasks. +3. **Open Forge**, point it at a git repo, and start dispatching tasks.
Antigravity CLI: run natively, not in Docker isolation @@ -132,8 +132,8 @@ The bundled image still ships the `agy` binary, and `~/.gemini/antigravity-cli` Build from source ```sh -git clone https://github.com/johannesjo/parallel-code.git -cd parallel-code +git clone https://github.com/johannesjo/forge.git +cd forge npm install npm run dev ``` @@ -176,7 +176,7 @@ Requires [Node.js](https://nodejs.org/) v18+. --- -If Parallel Code saves you time, consider giving it a [star on GitHub](https://github.com/johannesjo/parallel-code). It helps others find the project. +If Forge saves you time, consider giving it a [star on GitHub](https://github.com/johannesjo/forge). It helps others find the project. ## License diff --git a/build/logo-preview.html b/build/logo-preview.html index 4c6e4e27..4d033e85 100644 --- a/build/logo-preview.html +++ b/build/logo-preview.html @@ -35,7 +35,7 @@ font-size: 34px; color: #2ec8ff; " - >Parallel CodeForge diff --git a/build/logo-text-squared.svg b/build/logo-text-squared.svg index 5a84c20c..179de602 100644 --- a/build/logo-text-squared.svg +++ b/build/logo-text-squared.svg @@ -9,7 +9,7 @@ - + diff --git a/build/logo-text.svg b/build/logo-text.svg index ae50b074..f610b04f 100644 --- a/build/logo-text.svg +++ b/build/logo-text.svg @@ -10,7 +10,7 @@ - + diff --git a/docker/Dockerfile b/docker/Dockerfile index f775de19..e18c52b5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ -# Parallel Code — Docker agent image +# Forge — Docker agent image # Pre-installs common dev tools so agents don't waste time on setup. -# Build: docker build -t parallel-code-agent:latest docker/ +# Build: docker build -t forge-agent:latest docker/ # Size target: ~600 MB (multi-stage not needed; one fat layer is fine for a dev image) FROM ubuntu:22.04 diff --git a/electron/ipc/docker-config.test.ts b/electron/ipc/docker-config.test.ts index 8095f485..2b65b210 100644 --- a/electron/ipc/docker-config.test.ts +++ b/electron/ipc/docker-config.test.ts @@ -61,14 +61,14 @@ describe('selectMcpJsonDir — .mcp.json placement', () => { // ── copied mcp-server.cjs path ───────────────────────────────────────────────── describe('getDockerMcpServerDestPath — copied mcp-server.cjs location', () => { - it('places mcp-server.cjs in worktree .parallel-code dir', () => { + it('places mcp-server.cjs in worktree .forge dir', () => { const dest = getDockerMcpServerDestPath('/worktrees/coord', '/project'); - expect(dest).toBe('/worktrees/coord/.parallel-code/mcp-server.cjs'); + expect(dest).toBe('/worktrees/coord/.forge/mcp-server.cjs'); }); it('falls back to projectRoot when worktreePath is undefined', () => { const dest = getDockerMcpServerDestPath(undefined, '/project'); - expect(dest).toBe('/project/.parallel-code/mcp-server.cjs'); + expect(dest).toBe('/project/.forge/mcp-server.cjs'); }); it('dest is under the mounted worktree, not the unmounted projectRoot', () => { @@ -77,7 +77,7 @@ describe('getDockerMcpServerDestPath — copied mcp-server.cjs location', () => const dest = getDockerMcpServerDestPath(worktreePath, projectRoot); // The container mounts worktreePath (not projectRoot), so the script must live there expect(dest.startsWith(worktreePath)).toBe(true); - expect(dest.startsWith(projectRoot + '/.parallel-code')).toBe(false); + expect(dest.startsWith(projectRoot + '/.forge')).toBe(false); }); it('filename is always mcp-server.cjs', () => { @@ -90,7 +90,7 @@ describe('getDockerMcpServerDestPath — copied mcp-server.cjs location', () => describe('buildCoordinatorMCPConfig — config content', () => { const baseOpts = { - mcpServerPath: '/worktrees/coord/.parallel-code/mcp-server.cjs', + mcpServerPath: '/worktrees/coord/.forge/mcp-server.cjs', serverUrl: 'http://host.docker.internal:3001', token: 'test-token-abc', coordinatorTaskId: 'coord-task-1', @@ -98,19 +98,19 @@ describe('buildCoordinatorMCPConfig — config content', () => { it('has type:stdio and command:node', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - const server = cfg.mcpServers['parallel-code']; + const server = cfg.mcpServers['forge']; expect(server.type).toBe('stdio'); expect(server.command).toBe('node'); }); it('args[0] is the mcp-server.cjs path (the copied worktree path, not host path)', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - expect(cfg.mcpServers['parallel-code'].args[0]).toBe(baseOpts.mcpServerPath); + expect(cfg.mcpServers['forge'].args[0]).toBe(baseOpts.mcpServerPath); }); it('args contain --url pointing to host.docker.internal', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - const args = cfg.mcpServers['parallel-code'].args; + const args = cfg.mcpServers['forge'].args; const urlIdx = args.indexOf('--url'); expect(urlIdx).toBeGreaterThan(0); expect(args[urlIdx + 1]).toBe('http://host.docker.internal:3001'); @@ -118,14 +118,14 @@ describe('buildCoordinatorMCPConfig — config content', () => { it('token is passed via env var, not args', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - const args = cfg.mcpServers['parallel-code'].args; + const args = cfg.mcpServers['forge'].args; expect(args).not.toContain('--token'); - expect(cfg.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe(baseOpts.token); + expect(cfg.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe(baseOpts.token); }); it('args contain --coordinator-id', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - const args = cfg.mcpServers['parallel-code'].args; + const args = cfg.mcpServers['forge'].args; const coordIdx = args.indexOf('--coordinator-id'); expect(coordIdx).toBeGreaterThan(0); expect(args[coordIdx + 1]).toBe(baseOpts.coordinatorTaskId); @@ -133,7 +133,7 @@ describe('buildCoordinatorMCPConfig — config content', () => { it('omits --skip-permissions by default', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); - expect(cfg.mcpServers['parallel-code'].args).not.toContain('--skip-permissions'); + expect(cfg.mcpServers['forge'].args).not.toContain('--skip-permissions'); }); it('adds --skip-permissions when both flags are true', () => { @@ -142,7 +142,7 @@ describe('buildCoordinatorMCPConfig — config content', () => { skipPermissions: true, propagateSkipPermissions: true, }); - expect(cfg.mcpServers['parallel-code'].args).toContain('--skip-permissions'); + expect(cfg.mcpServers['forge'].args).toContain('--skip-permissions'); }); it('does NOT add --skip-permissions when propagateSkipPermissions is false', () => { @@ -151,7 +151,7 @@ describe('buildCoordinatorMCPConfig — config content', () => { skipPermissions: true, propagateSkipPermissions: false, }); - expect(cfg.mcpServers['parallel-code'].args).not.toContain('--skip-permissions'); + expect(cfg.mcpServers['forge'].args).not.toContain('--skip-permissions'); }); it('does NOT add --skip-permissions when skipPermissions is false', () => { @@ -160,13 +160,13 @@ describe('buildCoordinatorMCPConfig — config content', () => { skipPermissions: false, propagateSkipPermissions: true, }); - expect(cfg.mcpServers['parallel-code'].args).not.toContain('--skip-permissions'); + expect(cfg.mcpServers['forge'].args).not.toContain('--skip-permissions'); }); - it('JSON-serialised output is valid JSON with the parallel-code key', () => { + it('JSON-serialised output is valid JSON with the forge key', () => { const cfg = buildCoordinatorMCPConfig(baseOpts); const json = JSON.stringify(cfg, null, 2); const parsed = JSON.parse(json) as typeof cfg; - expect(parsed.mcpServers['parallel-code']).toBeDefined(); + expect(parsed.mcpServers['forge']).toBeDefined(); }); }); diff --git a/electron/ipc/git-symlink-excludes.test.ts b/electron/ipc/git-symlink-excludes.test.ts index 630e5a57..8380ec88 100644 --- a/electron/ipc/git-symlink-excludes.test.ts +++ b/electron/ipc/git-symlink-excludes.test.ts @@ -58,7 +58,7 @@ describe('ensureSymlinkExcludes', () => { expect(mockAppendFileSync).toHaveBeenCalledOnce(); const [, appended] = mockAppendFileSync.mock.calls[0] as [string, string]; - expect(appended).toContain('# parallel-code: worktree symlinks'); + expect(appended).toContain('# forge: worktree symlinks'); expect(appended).toContain('/node_modules'); }); @@ -108,12 +108,12 @@ describe('ensureSymlinkExcludes', () => { it('appends without repeating the header when it is already present', () => { mockExecFileSync.mockReturnValueOnce('.git\n'); - mockReadFileSync.mockReturnValueOnce('# parallel-code: worktree symlinks\n/existing\n'); + mockReadFileSync.mockReturnValueOnce('# forge: worktree symlinks\n/existing\n'); ensureSymlinkExcludes('/worktree', ['.env']); const [, appended] = mockAppendFileSync.mock.calls[0] as [string, string]; - expect(appended).not.toContain('# parallel-code: worktree symlinks'); + expect(appended).not.toContain('# forge: worktree symlinks'); expect(appended).toContain('/.env'); }); diff --git a/electron/ipc/git.ts b/electron/ipc/git.ts index 9c71a1be..add70298 100644 --- a/electron/ipc/git.ts +++ b/electron/ipc/git.ts @@ -162,7 +162,7 @@ const SANDBOX_EXCLUDE_PATTERNS = [ '/.zprofile', '/.zshrc', ]; -const SANDBOX_EXCLUDE_HEADER = '# parallel-code: sandbox bind-mount artifacts'; +const SANDBOX_EXCLUDE_HEADER = '# forge: sandbox bind-mount artifacts'; const seededSandboxExcludes = new Set(); /** @@ -170,7 +170,7 @@ const seededSandboxExcludes = new Set(); * symlinked name is appended individually on subsequent calls so new names * added in later worktrees are also covered. */ -const SYMLINK_EXCLUDE_HEADER = '# parallel-code: worktree symlinks'; +const SYMLINK_EXCLUDE_HEADER = '# forge: worktree symlinks'; // --- Internal helpers --- diff --git a/electron/ipc/pty.test.ts b/electron/ipc/pty.test.ts index 5655aa4f..7d8e7d79 100644 --- a/electron/ipc/pty.test.ts +++ b/electron/ipc/pty.test.ts @@ -127,7 +127,7 @@ function buildSpawnArgs( cols: 120, rows: 40, dockerMode: true, - dockerImage: 'parallel-code-agent:test', + dockerImage: 'forge-agent:test', shareDockerAgentAuth: false, onOutput: { __CHANNEL_ID__: 'channel-1' }, ...overrides, @@ -296,7 +296,7 @@ describe('spawnAgent docker mode', () => { expect(getFlagValues(ctx.args, '-e')).toContain(`HOME=`); expect(logged).not.toContain('secret-api-key'); expect(logged).not.toContain(`HOME=${DOCKER_CONTAINER_HOME}`); - expect(logged).toContain('parallel-code-agent:test'); + expect(logged).toContain('forge-agent:test'); }); it('redacts inline docker env values in spawn debug logs', () => { @@ -367,7 +367,7 @@ describe('spawnAgent docker mode', () => { const containerHome = `${DOCKER_CONTAINER_HOME}/agent-${agentId}`; const volumeFlags = getFlagValues(getLastSpawnCall().args, '-v'); - const expectedHostDir = `${home}/.parallel-code/agent-auth/${command}/${relDir}`; + const expectedHostDir = `${home}/.forge/agent-auth/${command}/${relDir}`; expect(volumeFlags).toContain(`${expectedHostDir}:${containerHome}/${relDir}`); }, ); @@ -381,7 +381,7 @@ describe('spawnAgent docker mode', () => { buildSpawnArgs({ command: 'claude', shareDockerAgentAuth: true }), ); - const hostDir = `${home}/.parallel-code/agent-auth/claude/.claude`; + const hostDir = `${home}/.forge/agent-auth/claude/.claude`; expect(fs.existsSync(hostDir)).toBe(true); }); @@ -397,7 +397,7 @@ describe('spawnAgent docker mode', () => { const containerHome = `${DOCKER_CONTAINER_HOME}/agent-${agentId}`; const volumeFlags = getFlagValues(getLastSpawnCall().args, '-v'); - const expectedHostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const expectedHostFile = `${home}/.forge/agent-auth/claude/.claude.json`; expect(volumeFlags).toContain(`${expectedHostFile}:${containerHome}/.claude.json`); expect(JSON.parse(fs.readFileSync(expectedHostFile, 'utf8'))).toMatchObject({ projects: { @@ -422,7 +422,7 @@ describe('spawnAgent docker mode', () => { }), ); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; const config = JSON.parse(fs.readFileSync(hostFile, 'utf8')) as { projects?: Record< string, @@ -438,7 +438,7 @@ describe('spawnAgent docker mode', () => { it('preserves existing Claude project config when pre-seeding folder trust', () => { const home = makeTempHome([]); vi.stubEnv('HOME', home); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; fs.mkdirSync(path.dirname(hostFile), { recursive: true }); fs.writeFileSync( hostFile, @@ -491,7 +491,7 @@ describe('spawnAgent docker mode', () => { ); const volumeFlags = getFlagValues(getLastSpawnCall().args, '-v'); - expect(volumeFlags.some((v) => v.includes('.parallel-code/agent-auth'))).toBe(false); + expect(volumeFlags.some((v) => v.includes('.forge/agent-auth'))).toBe(false); }); it('does not mount agent auth directory for an unknown agent command', () => { @@ -504,13 +504,13 @@ describe('spawnAgent docker mode', () => { ); const volumeFlags = getFlagValues(getLastSpawnCall().args, '-v'); - expect(volumeFlags.some((v) => v.includes('.parallel-code/agent-auth'))).toBe(false); + expect(volumeFlags.some((v) => v.includes('.forge/agent-auth'))).toBe(false); }); it('does not crash spawn when .claude.json contains malformed JSON', () => { const home = makeTempHome([]); vi.stubEnv('HOME', home); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; fs.mkdirSync(path.dirname(hostFile), { recursive: true }); fs.writeFileSync(hostFile, '{invalid json'); @@ -526,7 +526,7 @@ describe('spawnAgent docker mode', () => { it('preserves existing project config for other paths after trust seeding', () => { const home = makeTempHome([]); vi.stubEnv('HOME', home); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; fs.mkdirSync(path.dirname(hostFile), { recursive: true }); fs.writeFileSync( hostFile, @@ -581,7 +581,7 @@ describe('spawnAgent docker mode', () => { }), ); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; const config = JSON.parse(fs.readFileSync(hostFile, 'utf8')) as { projects?: Record< string, @@ -611,7 +611,7 @@ describe('spawnAgent docker mode', () => { }), ); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; expect(fs.existsSync(hostFile)).toBe(false); }); @@ -630,7 +630,7 @@ describe('spawnAgent docker mode', () => { ); // Verify trust is written to host file after first spawn - const claudeJsonPath = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const claudeJsonPath = `${home}/.forge/agent-auth/claude/.claude.json`; const afterFirst = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8')) as { projects: Record; }; @@ -815,18 +815,18 @@ describe('validateCommand', () => { }); describe('resolveProjectDockerfile', () => { - it('returns absolute path when .parallel-code/Dockerfile exists in project root', () => { + it('returns absolute path when .forge/Dockerfile exists in project root', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pty-resolve-')); tempPaths.push(projectRoot); - const dockerDir = path.join(projectRoot, '.parallel-code'); + const dockerDir = path.join(projectRoot, '.forge'); fs.mkdirSync(dockerDir, { recursive: true }); fs.writeFileSync(path.join(dockerDir, 'Dockerfile'), 'FROM node:20\n'); const result = resolveProjectDockerfile(projectRoot); - expect(result).toBe(path.join(projectRoot, '.parallel-code', 'Dockerfile')); + expect(result).toBe(path.join(projectRoot, '.forge', 'Dockerfile')); }); - it('returns null when .parallel-code/Dockerfile does not exist', () => { + it('returns null when .forge/Dockerfile does not exist', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pty-resolve-')); tempPaths.push(projectRoot); @@ -839,10 +839,10 @@ describe('resolveProjectDockerfile', () => { expect(result).toBeNull(); }); - it('returns null when .parallel-code/Dockerfile is a directory', () => { + it('returns null when .forge/Dockerfile is a directory', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pty-resolve-')); tempPaths.push(projectRoot); - fs.mkdirSync(path.join(projectRoot, '.parallel-code', 'Dockerfile'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, '.forge', 'Dockerfile'), { recursive: true }); const result = resolveProjectDockerfile(projectRoot); expect(result).toBeNull(); @@ -850,19 +850,19 @@ describe('resolveProjectDockerfile', () => { }); describe('projectImageTag', () => { - it('returns a tag in the format parallel-code-project:<12-char-hash>', () => { + it('returns a tag in the format forge-project:<12-char-hash>', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pty-tag-')); tempPaths.push(tmpDir); const dockerfilePath = path.join(tmpDir, 'Dockerfile'); fs.writeFileSync(dockerfilePath, 'FROM node:20\nRUN echo hello\n'); const tag = projectImageTag(dockerfilePath); - expect(tag).toMatch(/^parallel-code-project:[a-f0-9]{12}$/); + expect(tag).toMatch(/^forge-project:[a-f0-9]{12}$/); }); - it('returns parallel-code-project:unknown for non-existent Dockerfile path', () => { + it('returns forge-project:unknown for non-existent Dockerfile path', () => { const tag = projectImageTag('/nonexistent/Dockerfile'); - expect(tag).toBe('parallel-code-project:unknown'); + expect(tag).toBe('forge-project:unknown'); }); }); @@ -896,7 +896,7 @@ describe('dockerImageExists', () => { ); await expect( - dockerImageExists('parallel-code-project:test', { + dockerImageExists('forge-project:test', { dockerfilePath: '/nonexistent/Dockerfile', }), ).resolves.toBe(false); @@ -907,14 +907,14 @@ describe('buildDockerImage', () => { it('uses the provided build context for a project dockerfile', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pty-build-context-')); tempPaths.push(projectRoot); - const dockerDir = path.join(projectRoot, '.parallel-code'); + const dockerDir = path.join(projectRoot, '.forge'); fs.mkdirSync(dockerDir, { recursive: true }); const dockerfilePath = path.join(dockerDir, 'Dockerfile'); fs.writeFileSync(dockerfilePath, 'FROM node:20\n'); buildDockerImage(createMockWindow(), 'channel:build-test', { dockerfilePath, - imageTag: 'parallel-code-project:test', + imageTag: 'forge-project:test', buildContext: projectRoot, } as unknown as Parameters[2]); @@ -928,7 +928,7 @@ describe('buildDockerImage', () => { describe('killAgent — Docker container lifecycle', () => { it('calls docker stop with the predictable container name when agent is killed', () => { const agentId = nextAgentId(); - const containerName = `parallel-code-${agentId.slice(0, 12)}`; + const containerName = `forge-${agentId.slice(0, 12)}`; spawnAgent(createMockWindow(), buildSpawnArgs({ agentId })); killAgent(agentId); @@ -940,14 +940,14 @@ describe('killAgent — Docker container lifecycle', () => { expect(stopCall?.[1]).toContain(containerName); }); - it('container name is always parallel-code-', () => { + it('container name is always forge-', () => { const agentId = 'agent-abcdef-ghij-klmn'; - const expected = `parallel-code-${agentId.slice(0, 12)}`; - expect(expected).toBe('parallel-code-agent-abcdef'); + const expected = `forge-${agentId.slice(0, 12)}`; + expect(expected).toBe('forge-agent-abcdef'); // The container name must be deterministic and predictable for cleanup // (no random suffix) so we can always `docker stop` it by name. - expect(expected.startsWith('parallel-code-')).toBe(true); - expect(expected.length).toBe(14 + 12); // 'parallel-code-' + 12 chars + expect(expected.startsWith('forge-')).toBe(true); + expect(expected.length).toBe(6 + 12); // 'forge-' + 12 chars }); it('does not call docker stop for a non-Docker agent', () => { @@ -1024,7 +1024,7 @@ describe('seedClaudeProjectTrust — concurrent spawns', () => { }), ); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; const config = JSON.parse(fs.readFileSync(hostFile, 'utf8')) as { projects: Record; }; @@ -1161,7 +1161,7 @@ describe('spawnAgent docker mode — path edge cases', () => { }), ); - const claudeJson = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const claudeJson = `${home}/.forge/agent-auth/claude/.claude.json`; // .claude.json must not be created for non-Claude agents expect(fs.existsSync(claudeJson)).toBe(false); }); @@ -1183,7 +1183,7 @@ describe('seedClaudeProjectTrust — file permissions', () => { }), ); - const hostFile = `${home}/.parallel-code/agent-auth/claude/.claude.json`; + const hostFile = `${home}/.forge/agent-auth/claude/.claude.json`; expect(fs.existsSync(hostFile)).toBe(true); const stat = fs.statSync(hostFile); // mode & 0o777 strips file-type bits; 0o600 = owner r/w, no group/other access @@ -1199,7 +1199,7 @@ describe('buildDockerCredentialMounts — read-only auth dir', () => { vi.stubEnv('HOME', home); // Create the parent as a file to block mkdirSync - const authBase = path.join(home, '.parallel-code'); + const authBase = path.join(home, '.forge'); fs.mkdirSync(authBase, { recursive: true }); // Create 'agent-auth' as a file so mkdirSync for 'claude' inside it will fail fs.writeFileSync(path.join(authBase, 'agent-auth'), 'not-a-dir'); diff --git a/electron/ipc/pty.ts b/electron/ipc/pty.ts index 5cf49440..2da38d26 100644 --- a/electron/ipc/pty.ts +++ b/electron/ipc/pty.ts @@ -259,7 +259,7 @@ export function spawnAgent( 'NODE_OPTIONS', 'ELECTRON_RUN_AS_NODE', // Prevent renderer from injecting or overriding MCP auth tokens. - 'PARALLEL_CODE_MCP_TOKEN', + 'FORGE_MCP_TOKEN', ]); const safeEnvOverrides: Record = {}; for (const [k, v] of Object.entries(args.env ?? {})) { @@ -290,7 +290,7 @@ export function spawnAgent( // Derive a predictable, unique container name from the agentId so we can // reliably stop it later without having to parse docker inspect output. - const containerName = args.dockerMode ? `parallel-code-${args.agentId.slice(0, 12)}` : null; + const containerName = args.dockerMode ? `forge-${args.agentId.slice(0, 12)}` : null; if (args.dockerMode) { const name = containerName as string; @@ -305,7 +305,7 @@ export function spawnAgent( name, // Label so we can identify all containers owned by this app '--label', - 'parallel-code=true', + 'forge=true', // Host networking — agents need internet access for API calls and package installs. // Filesystem isolation (volume mounts) is the primary safety goal, not network isolation. '--network', @@ -806,7 +806,7 @@ function buildDockerCredentialMounts( if (shareAgentAuth) { const baseCommand = path.basename(agentCommand); for (const relDir of AGENT_CONFIG_DIRS[baseCommand] ?? []) { - const hostDir = path.join(home, '.parallel-code', 'agent-auth', baseCommand, relDir); + const hostDir = path.join(home, '.forge', 'agent-auth', baseCommand, relDir); try { fs.mkdirSync(hostDir, { recursive: true, mode: 0o700 }); mounts.push('-v', `${hostDir}:${containerHome}/${relDir}`); @@ -815,7 +815,7 @@ function buildDockerCredentialMounts( } } for (const relFile of AGENT_CONFIG_FILES[baseCommand] ?? []) { - const hostFile = path.join(home, '.parallel-code', 'agent-auth', baseCommand, relFile); + const hostFile = path.join(home, '.forge', 'agent-auth', baseCommand, relFile); try { const hostDir = path.dirname(hostFile); fs.mkdirSync(hostDir, { recursive: true, mode: 0o700 }); @@ -856,10 +856,10 @@ export async function isDockerAvailable(): Promise { } /** The default image name for Docker-isolated tasks. */ -export const DOCKER_DEFAULT_IMAGE = 'parallel-code-agent:latest'; +export const DOCKER_DEFAULT_IMAGE = 'forge-agent:latest'; /** Label key used to stamp the Dockerfile content hash on built images. */ -const DOCKERFILE_HASH_LABEL = 'parallel-code-dockerfile-hash'; +const DOCKERFILE_HASH_LABEL = 'forge-dockerfile-hash'; /** * Resolve the path to the bundled Dockerfile. @@ -893,11 +893,11 @@ function getDockerfileHash(): string | null { } /** - * Check if a project has a local Dockerfile at .parallel-code/Dockerfile. + * Check if a project has a local Dockerfile at .forge/Dockerfile. * Returns the absolute path if found, null otherwise. */ export function resolveProjectDockerfile(projectRoot: string): string | null { - const p = path.join(projectRoot, '.parallel-code', 'Dockerfile'); + const p = path.join(projectRoot, '.forge', 'Dockerfile'); try { return fs.statSync(p).isFile() ? p : null; } catch { @@ -907,11 +907,11 @@ export function resolveProjectDockerfile(projectRoot: string): string | null { /** * Derive a deterministic image tag for a project Dockerfile. - * Tag format: parallel-code-project: + * Tag format: forge-project: */ export function projectImageTag(dockerfilePath: string): string { const hash = hashDockerfile(dockerfilePath); - return `parallel-code-project:${(hash ?? 'unknown').slice(0, 12)}`; + return `forge-project:${(hash ?? 'unknown').slice(0, 12)}`; } /** diff --git a/electron/ipc/register-mcp.test.ts b/electron/ipc/register-mcp.test.ts index 0f5d952a..31f34be0 100644 --- a/electron/ipc/register-mcp.test.ts +++ b/electron/ipc/register-mcp.test.ts @@ -28,7 +28,7 @@ const TEST_COORDINATOR_ID = '12345678-1234-4234-8234-123456789abc'; const tempDirs: string[] = []; function mkTemp(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'parallel-code-mcp-test-')); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-mcp-test-')); tempDirs.push(dir); return dir; } @@ -83,7 +83,7 @@ describe('Layer 3 — MCP startup pipeline (no Electron, real FS)', () => { expect(fs.existsSync(destPath)).toBe(true); const written = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')) as typeof mcpConfig; - const server = written.mcpServers['parallel-code']; + const server = written.mcpServers['forge']; // Server path in config points to the COPIED file inside the worktree, not the original expect(server.args[0]).toBe(destPath); @@ -96,10 +96,10 @@ describe('Layer 3 — MCP startup pipeline (no Electron, real FS)', () => { // Token is passed via env var, not args expect(server.args).not.toContain('--token'); - expect(server.env['PARALLEL_CODE_MCP_TOKEN']).toBe(token); + expect(server.env['FORGE_MCP_TOKEN']).toBe(token); }); - it('mcp-server.cjs is copied to .parallel-code inside the worktree', () => { + it('mcp-server.cjs is copied to .forge inside the worktree', () => { const worktreePath = mkTemp(); const projectRoot = mkTemp(); const fakeSrc = path.join(mkTemp(), 'mcp-server.cjs'); @@ -109,9 +109,9 @@ describe('Layer 3 — MCP startup pipeline (no Electron, real FS)', () => { fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.copyFileSync(fakeSrc, destPath); - // Must be inside worktree, under .parallel-code/ + // Must be inside worktree, under .forge/ expect(destPath.startsWith(worktreePath)).toBe(true); - expect(destPath).toContain('/.parallel-code/mcp-server.cjs'); + expect(destPath).toContain('/.forge/mcp-server.cjs'); expect(fs.existsSync(destPath)).toBe(true); }); @@ -169,7 +169,7 @@ describe('Layer 3 — MCP startup pipeline (no Electron, real FS)', () => { const written = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')) as typeof cfg; // URL is 127.0.0.1 (non-Docker) - const args = written.mcpServers['parallel-code'].args; + const args = written.mcpServers['forge'].args; const urlIdx = args.indexOf('--url'); expect(args[urlIdx + 1]).toBe('http://127.0.0.1:3001'); // Server path is the host path (no copy) @@ -182,7 +182,7 @@ describe('Layer 3 — MCP startup pipeline (no Electron, real FS)', () => { // ───────────────────────────────────────────────────────────────────────────── describe('Layer 3b — .mcp.json merge and cleanup', () => { - it('merges parallel-code into a pre-existing .mcp.json preserving other servers', () => { + it('merges forge into a pre-existing .mcp.json preserving other servers', () => { const worktreePath = mkTemp(); const projectRoot = mkTemp(); const mcpJsonDir = selectMcpJsonDir(worktreePath, projectRoot); @@ -207,10 +207,10 @@ describe('Layer 3b — .mcp.json merge and cleanup', () => { const written = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')) as typeof parsed; expect(written.mcpServers?.['my-server']).toEqual({ command: 'my-tool', args: [] }); - expect(written.mcpServers?.['parallel-code']).toBeDefined(); + expect(written.mcpServers?.['forge']).toBeDefined(); }); - it('deregister removes parallel-code key and preserves other servers', () => { + it('deregister removes forge key and preserves other servers', () => { const worktreePath = mkTemp(); const projectRoot = mkTemp(); const mcpJsonDir = selectMcpJsonDir(worktreePath, projectRoot); @@ -220,7 +220,7 @@ describe('Layer 3b — .mcp.json merge and cleanup', () => { const twoServers = { mcpServers: { 'my-server': { command: 'my-tool', args: [] }, - 'parallel-code': { command: 'node', args: ['server.cjs'] }, + forge: { command: 'node', args: ['server.cjs'] }, }, }; fs.writeFileSync(mcpJsonPath, JSON.stringify(twoServers, null, 2)); @@ -228,7 +228,7 @@ describe('Layer 3b — .mcp.json merge and cleanup', () => { // Simulate deregisterCoordinator cleanup logic const raw = fs.readFileSync(mcpJsonPath, 'utf-8'); const content = JSON.parse(raw) as { mcpServers?: Record }; - if (content.mcpServers) delete content.mcpServers['parallel-code']; + if (content.mcpServers) delete content.mcpServers['forge']; const hasServers = Object.keys(content.mcpServers ?? {}).length > 0; const hasOtherKeys = Object.keys(content).filter((k) => k !== 'mcpServers').length > 0; if (!hasServers && !hasOtherKeys) { @@ -241,21 +241,21 @@ describe('Layer 3b — .mcp.json merge and cleanup', () => { expect(fs.existsSync(mcpJsonPath)).toBe(true); const written = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')) as typeof content; expect(written.mcpServers?.['my-server']).toEqual({ command: 'my-tool', args: [] }); - expect(written.mcpServers?.['parallel-code']).toBeUndefined(); + expect(written.mcpServers?.['forge']).toBeUndefined(); }); - it('deregister deletes the file when parallel-code was the only entry', () => { + it('deregister deletes the file when forge was the only entry', () => { const worktreePath = mkTemp(); const projectRoot = mkTemp(); const mcpJsonDir = selectMcpJsonDir(worktreePath, projectRoot); const mcpJsonPath = path.join(mcpJsonDir, '.mcp.json'); - const onlyUs = { mcpServers: { 'parallel-code': { command: 'node', args: [] } } }; + const onlyUs = { mcpServers: { forge: { command: 'node', args: [] } } }; fs.writeFileSync(mcpJsonPath, JSON.stringify(onlyUs, null, 2)); const raw = fs.readFileSync(mcpJsonPath, 'utf-8'); const content = JSON.parse(raw) as { mcpServers?: Record }; - if (content.mcpServers) delete content.mcpServers['parallel-code']; + if (content.mcpServers) delete content.mcpServers['forge']; const hasServers = Object.keys(content.mcpServers ?? {}).length > 0; const hasOtherKeys = Object.keys(content).filter((k) => k !== 'mcpServers').length > 0; if (!hasServers && !hasOtherKeys) { @@ -487,7 +487,7 @@ describe('Layer 5 — Failure modes', () => { token: 'tok', coordinatorTaskId: TEST_COORDINATOR_ID, }); - const args = staleCfg.mcpServers['parallel-code'].args; + const args = staleCfg.mcpServers['forge'].args; const urlIdx = args.indexOf('--url'); const url = args[urlIdx + 1]; diff --git a/electron/ipc/register.ts b/electron/ipc/register.ts index f1e4ee69..582e4014 100644 --- a/electron/ipc/register.ts +++ b/electron/ipc/register.ts @@ -113,7 +113,7 @@ export function getDockerMcpServerDestPath( worktreePath: string | undefined, projectRoot: string, ): string { - return path.join(worktreePath ?? projectRoot, '.parallel-code', 'mcp-server.cjs'); + return path.join(worktreePath ?? projectRoot, '.forge', 'mcp-server.cjs'); } export interface CoordinatorMCPConfigOpts { @@ -128,7 +128,7 @@ export interface CoordinatorMCPConfigOpts { /** Builds the coordinator MCP server config used for launch args and `.mcp.json`. */ export function buildCoordinatorMCPConfig(opts: CoordinatorMCPConfigOpts): { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio'; command: 'node'; args: string[]; @@ -138,7 +138,7 @@ export function buildCoordinatorMCPConfig(opts: CoordinatorMCPConfigOpts): { } { return { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio', command: 'node', args: [ @@ -149,7 +149,7 @@ export function buildCoordinatorMCPConfig(opts: CoordinatorMCPConfigOpts): { opts.coordinatorTaskId, ...(opts.skipPermissions && opts.propagateSkipPermissions ? ['--skip-permissions'] : []), ], - env: { PARALLEL_CODE_MCP_TOKEN: opts.token }, + env: { FORGE_MCP_TOKEN: opts.token }, }, }, }; @@ -327,8 +327,8 @@ function sanitizeDroppedName(name: string): string { .trim() .slice(0, 200); const stamp = `${Date.now()}-${crypto.randomBytes(3).toString('hex')}`; - if (base) return `parallel-code-drop-${stamp}-${base}`; - return `parallel-code-drop-${stamp}.png`; + if (base) return `forge-drop-${stamp}-${base}`; + return `forge-drop-${stamp}.png`; } /** @@ -951,7 +951,7 @@ export function registerAllHandlers(win: BrowserWindow): void { }); // --- Clipboard --- - const clipboardImagePath = path.join(os.tmpdir(), 'parallel-code-clipboard.png'); + const clipboardImagePath = path.join(os.tmpdir(), 'forge-clipboard.png'); // Resolve the most useful representation of the current clipboard contents // for pasting into a terminal. Order of preference: @@ -1344,7 +1344,7 @@ export function registerAllHandlers(win: BrowserWindow): void { // Clean up the host-temp MCP config file written by StartMCPServer (non-Docker only). const tempConfigPath = path.join( app.getPath('temp'), - `parallel-code-mcp-${args.coordinatorTaskId}.json`, + `forge-mcp-${args.coordinatorTaskId}.json`, ); try { fs.unlinkSync(tempConfigPath); @@ -1637,7 +1637,7 @@ export function registerAllHandlers(win: BrowserWindow): void { // merge logic ever grows fallible, Docker residue is never left behind. const mcpConfig = { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio' as const, command: 'node', args: [ @@ -1650,7 +1650,7 @@ export function registerAllHandlers(win: BrowserWindow): void { ? ['--skip-permissions'] : []), ], - env: { PARALLEL_CODE_MCP_TOKEN: remoteServer.token }, + env: { FORGE_MCP_TOKEN: remoteServer.token }, }, }, }; @@ -1659,11 +1659,11 @@ export function registerAllHandlers(win: BrowserWindow): void { // Merge mcpConfig into the pre-validated existingMcpContent (parsed above, // before any coordinator state was mutated). - // Capture the previous parallel-code entry so deregistration can restore it instead of + // Capture the previous forge entry so deregistration can restore it instead of // unconditionally deleting it (which would remove a user-owned entry). const existingServers = (existingMcpContent.mcpServers as Record | undefined) ?? {}; - const previousMcpParallelCode: unknown = existingServers['parallel-code']; + const previousMcpForge: unknown = existingServers['forge']; let mergedMcpJson: string | undefined; if (mcpJsonDir && worktreeMcpPath) { existingMcpContent.mcpServers = { ...existingServers, ...mcpConfig.mcpServers }; @@ -1678,13 +1678,13 @@ export function registerAllHandlers(win: BrowserWindow): void { coordinator.setDockerContainerName(args.coordinatorTaskId, args.dockerContainerName ?? ''); coordinator.setDockerImage(args.coordinatorTaskId, args.dockerImage ?? null); console.warn('[MCP] Docker mode: copied MCP server to', dockerMcpServerPath); - // Keep .parallel-code/ out of git status in the sub-task worktree. + // Keep .forge/ out of git status in the sub-task worktree. // Use .git/info/exclude (local-only, never committed) to avoid dirtying // a tracked .gitignore file on every Docker coordinator startup. appendGitExclude( args.worktreePath ?? args.projectRoot, - '.parallel-code/', - '\n# Parallel Code Docker MCP dir\n.parallel-code/\n', + '.forge/', + '\n# Forge Docker MCP dir\n.forge/\n', ); } else { coordinator.setDockerContainerName(args.coordinatorTaskId, null); @@ -1707,32 +1707,29 @@ export function registerAllHandlers(win: BrowserWindow): void { // No host-temp configPath needed. let configPath: string | undefined; if (!args.dockerContainerName) { - configPath = path.join( - app.getPath('temp'), - `parallel-code-mcp-${args.coordinatorTaskId}.json`, - ); + configPath = path.join(app.getPath('temp'), `forge-mcp-${args.coordinatorTaskId}.json`); atomicWriteFileSync(configPath, configJson, { mode: 0o600 }); } // Write .mcp.json for auto-discovery. Read before writing — merge only the - // parallel-code key so we don't destroy user-defined entries. Track whether + // forge key so we don't destroy user-defined entries. Track whether // we created the file so deregisterCoordinator can clean up correctly. if (mcpJsonDir && worktreeMcpPath && mergedMcpJson !== undefined) { atomicWriteFileSync(worktreeMcpPath, mergedMcpJson, { mode: 0o600 }); - const writtenMcpParallelCode: unknown = mcpConfig.mcpServers['parallel-code']; + const writtenMcpForge: unknown = mcpConfig.mcpServers['forge']; coordinator.setMcpJsonInfo( args.coordinatorTaskId, worktreeMcpPath, !mcpFileExistedBefore, - previousMcpParallelCode, - writtenMcpParallelCode, + previousMcpForge, + writtenMcpForge, ); // Append to .git/info/exclude (local-only gitignore, not committed) appendGitExclude( mcpJsonDir, '.mcp.json', - '\n# Parallel Code MCP config (contains ephemeral token)\n.mcp.json\n', + '\n# Forge MCP config (contains ephemeral token)\n.mcp.json\n', ); console.warn('[MCP] .mcp.json written to:', worktreeMcpPath); diff --git a/electron/mcp/agent-args.test.ts b/electron/mcp/agent-args.test.ts index c7fa90b6..b14d6c66 100644 --- a/electron/mcp/agent-args.test.ts +++ b/electron/mcp/agent-args.test.ts @@ -10,11 +10,11 @@ import { const config = { mcpServers: { - 'parallel-code': { + forge: { command: 'node', args: ['/tmp/mcp-server.cjs', '--url', 'http://127.0.0.1:1234', '--coordinator-id', 'task-1'], env: { - PARALLEL_CODE_MCP_TOKEN: 'token-1', + FORGE_MCP_TOKEN: 'token-1', }, }, }, @@ -37,7 +37,7 @@ describe('MCP agent launch args', () => { it('quotes Codex inline config env keys so non-bare TOML keys remain valid', () => { const override = buildCodexMcpConfigOverride({ mcpServers: { - 'parallel-code': { + forge: { command: 'node', args: [], env: { diff --git a/electron/mcp/agent-args.ts b/electron/mcp/agent-args.ts index 5add7eab..4255efe9 100644 --- a/electron/mcp/agent-args.ts +++ b/electron/mcp/agent-args.ts @@ -1,12 +1,12 @@ -interface ParallelCodeMcpServerConfig { +interface ForgeMcpServerConfig { command: string; args: string[]; env: Record; } -export interface ParallelCodeMcpConfig { +export interface ForgeMcpConfig { mcpServers: { - 'parallel-code': ParallelCodeMcpServerConfig; + forge: ForgeMcpServerConfig; }; } @@ -36,15 +36,15 @@ function tomlStringMap(values: Record): string { .join(', ')} }`; } -export function buildCodexMcpConfigOverride(config: ParallelCodeMcpConfig): string { - const server = config.mcpServers['parallel-code']; - return `mcp_servers.parallel-code={ command = ${tomlString(server.command)}, args = ${tomlStringArray(server.args)}, env = ${tomlStringMap(server.env)} }`; +export function buildCodexMcpConfigOverride(config: ForgeMcpConfig): string { + const server = config.mcpServers['forge']; + return `mcp_servers.forge={ command = ${tomlString(server.command)}, args = ${tomlStringArray(server.args)}, env = ${tomlStringMap(server.env)} }`; } export function buildMcpLaunchArgs( command: string, configPath: string | undefined, - config: ParallelCodeMcpConfig, + config: ForgeMcpConfig, ): string[] { if (isCodexCommand(command)) { return ['--config', buildCodexMcpConfigOverride(config)]; diff --git a/electron/mcp/agent-frame-fixtures.ts b/electron/mcp/agent-frame-fixtures.ts index ad78a1a4..44bab90d 100644 --- a/electron/mcp/agent-frame-fixtures.ts +++ b/electron/mcp/agent-frame-fixtures.ts @@ -14,7 +14,7 @@ export const READY_AGENT_FRAME_FIXTURES: AgentFrameFixture[] = [ '│ >', '❯', '', - 'opus · /Users/brooksc/git/parallel-code/.worktrees/task-023-10-loading-states · ctx:24k/200k', + 'opus · /Users/brooksc/git/forge/.worktrees/task-023-10-loading-states · ctx:24k/200k', ].join('\n'), }, { @@ -22,7 +22,7 @@ export const READY_AGENT_FRAME_FIXTURES: AgentFrameFixture[] = [ frame: [ '│ >', '-- INSERT --', - 'opus · /Users/brooksc/git/parallel-code/.worktrees/task-029-linting · ctx:0/200k', + 'opus · /Users/brooksc/git/forge/.worktrees/task-029-linting · ctx:0/200k', ].join('\n'), }, { @@ -33,7 +33,7 @@ export const READY_AGENT_FRAME_FIXTURES: AgentFrameFixture[] = [ '❯ ', '────────────────────────────────────────────────────────────────', '--INSERT--⏵⏵ bypass permissions on (shift+tab to cycle)', - 'Sonnet 4 | ~/git/parallel-code/.worktrees/task/example', + 'Sonnet 4 | ~/git/forge/.worktrees/task/example', ].join('\r'), }, { @@ -41,13 +41,13 @@ export const READY_AGENT_FRAME_FIXTURES: AgentFrameFixture[] = [ frame: [ '›', '', - 'gpt-5.5 default · /Users/brooksc/git/parallel-code/.worktrees/task-028-unit-tests', + 'gpt-5.5 default · /Users/brooksc/git/forge/.worktrees/task-028-unit-tests', ].join('\n'), }, { name: 'Gemini typed-message prompt', frame: [ - 'workspace /Users/brooksc/git/parallel-code/.worktrees/task-gemini branch sandbox', + 'workspace /Users/brooksc/git/forge/.worktrees/task-gemini branch sandbox', '> Type your message or @path/to/file', ].join('\n'), }, @@ -59,18 +59,16 @@ export const NOT_READY_AGENT_FRAME_FIXTURES: NotReadyAgentFrameFixture[] = [ reason: 'busy', frame: [ '› Implement the requested fix', - 'gpt-5.5 default · /Users/brooksc/git/parallel-code/.worktrees/task-028-unit-tests', + 'gpt-5.5 default · /Users/brooksc/git/forge/.worktrees/task-028-unit-tests', 'Working (18m 51s • esc to interrupt) • 1 background terminal running • /stop to close', ].join('\n'), }, { name: 'Codex MCP startup screen', reason: 'startup_or_dialog', - frame: [ - 'Starting MCP servers (0/2): codex_apps, parallel-code', - 'Booting MCP server parallel-code', - '›', - ].join('\n'), + frame: ['Starting MCP servers (0/2): codex_apps, forge', 'Booting MCP server forge', '›'].join( + '\n', + ), }, { name: 'Agent trust dialog', diff --git a/electron/mcp/atomic.ts b/electron/mcp/atomic.ts index 14760ec7..ee40d693 100644 --- a/electron/mcp/atomic.ts +++ b/electron/mcp/atomic.ts @@ -77,7 +77,7 @@ export function atomicWriteFileSync( options?: { mode?: number }, ): void { const mode = resolveMode(filePath, options?.mode); - const tmp = join(dirname(filePath), `.parallel-code-atomic-${randomUUID()}.tmp`); + const tmp = join(dirname(filePath), `.forge-atomic-${randomUUID()}.tmp`); let fd = -1; try { fd = openSync(tmp, 'w', mode); // pre-set mode; still subject to umask @@ -114,7 +114,7 @@ export async function atomicWriteFile( options?: { mode?: number }, ): Promise { const mode = await resolveModeAsync(filePath, options?.mode); - const tmp = join(dirname(filePath), `.parallel-code-atomic-${randomUUID()}.tmp`); + const tmp = join(dirname(filePath), `.forge-atomic-${randomUUID()}.tmp`); let fh: Awaited> | undefined; try { fh = await open(tmp, 'w', mode); // pre-set mode; still subject to umask diff --git a/electron/mcp/config.test.ts b/electron/mcp/config.test.ts index 1edb5bda..9715f9b4 100644 --- a/electron/mcp/config.test.ts +++ b/electron/mcp/config.test.ts @@ -13,44 +13,42 @@ describe('getMCPRemoteServerUrl', () => { }); it('uses host.docker.internal on macOS Docker Desktop', () => { - expect(getMCPRemoteServerUrl(7777, 'parallel-code-container', 'darwin')).toBe( + expect(getMCPRemoteServerUrl(7777, 'forge-container', 'darwin')).toBe( 'http://host.docker.internal:7777', ); }); it('uses localhost on Linux (--network host shares host loopback)', () => { - expect(getMCPRemoteServerUrl(7777, 'parallel-code-container', 'linux')).toBe( - 'http://127.0.0.1:7777', - ); + expect(getMCPRemoteServerUrl(7777, 'forge-container', 'linux')).toBe('http://127.0.0.1:7777'); }); }); describe('getSubTaskMcpConfigPath', () => { - it('in Docker mode, places config in coordinator .parallel-code dir (the explicit volume)', () => { - const serverPath = '/worktree/.parallel-code/mcp-server.cjs'; + it('in Docker mode, places config in coordinator .forge dir (the explicit volume)', () => { + const serverPath = '/worktree/.forge/mcp-server.cjs'; expect(getSubTaskMcpConfigPath('my-container', serverPath, 'task-abc')).toBe( - '/worktree/.parallel-code/subtask-task-abc.json', + '/worktree/.forge/subtask-task-abc.json', ); }); it('in Docker mode, never places config in the sub-task worktree (not a volume mount)', () => { - const serverPath = '/coordinator-worktree/.parallel-code/mcp-server.cjs'; + const serverPath = '/coordinator-worktree/.forge/mcp-server.cjs'; const result = getSubTaskMcpConfigPath('my-container', serverPath, 'task-abc'); expect(result).not.toContain('sub-task-worktree'); - expect(result).toContain('.parallel-code'); + expect(result).toContain('.forge'); }); it('in host mode, places config in the OS temp directory', () => { - const serverPath = '/usr/lib/parallel-code/mcp-server.cjs'; + const serverPath = '/usr/lib/forge/mcp-server.cjs'; expect(getSubTaskMcpConfigPath(null, serverPath, 'task-xyz', '/tmp')).toBe( - '/tmp/parallel-code-subtask-task-xyz.json', + '/tmp/forge-subtask-task-xyz.json', ); }); it('in host mode with no container, uses OS tmpdir default', () => { - const serverPath = '/usr/lib/parallel-code/mcp-server.cjs'; + const serverPath = '/usr/lib/forge/mcp-server.cjs'; const result = getSubTaskMcpConfigPath(undefined, serverPath, 'task-123'); - expect(result).toBe(join(tmpdir(), 'parallel-code-subtask-task-123.json')); + expect(result).toBe(join(tmpdir(), 'forge-subtask-task-123.json')); }); }); diff --git a/electron/mcp/config.ts b/electron/mcp/config.ts index bcb0b4ba..cc998722 100644 --- a/electron/mcp/config.ts +++ b/electron/mcp/config.ts @@ -18,7 +18,7 @@ export function getMCPRemoteServerUrl( * * In Docker mode the sub-task worktree is NOT an explicit volume mount, so * auto-discovery inside the container is unreliable. Instead, write the config - * to the coordinator's .parallel-code/ dir (same dir as mcp-server.cjs) which + * to the coordinator's .forge/ dir (same dir as mcp-server.cjs) which * IS the explicit volume, and always pass it via --mcp-config. * * In host mode, use the OS temp directory (existing behaviour). @@ -31,7 +31,7 @@ export function getSubTaskMcpConfigPath( ): string { return dockerContainerName ? join(dirname(serverPath), `subtask-${taskId}.json`) - : join(tempDir, `parallel-code-subtask-${taskId}.json`); + : join(tempDir, `forge-subtask-${taskId}.json`); } /** diff --git a/electron/mcp/coordinator-real-agents.integration.test.ts b/electron/mcp/coordinator-real-agents.integration.test.ts index 03e8ed81..2adc76c0 100644 --- a/electron/mcp/coordinator-real-agents.integration.test.ts +++ b/electron/mcp/coordinator-real-agents.integration.test.ts @@ -44,11 +44,11 @@ function runGit(cwd: string, args: string[]): void { } function createRepo(): string { - const repo = mkdtempSync(join(tmpdir(), 'parallel-code-real-agent-repo-')); + const repo = mkdtempSync(join(tmpdir(), 'forge-real-agent-repo-')); runGit(repo, ['init']); runGit(repo, ['checkout', '-b', 'main']); - runGit(repo, ['config', 'user.email', 'parallel-code-test@example.com']); - runGit(repo, ['config', 'user.name', 'Parallel Code Test']); + runGit(repo, ['config', 'user.email', 'forge-test@example.com']); + runGit(repo, ['config', 'user.name', 'Forge Test']); writeFileSync(join(repo, 'README.md'), '# real agent smoke\n'); runGit(repo, ['add', 'README.md']); runGit(repo, ['commit', '-m', 'initial']); @@ -161,13 +161,13 @@ describeRealAgents('Coordinator real agent startup smoke', () => { const coordinator = new Coordinator(); const rendererEvents: RendererEvent[] = []; let taskId: string | undefined; - const token = `PARALLEL_CODE_REAL_AGENT_SMOKE_${name.toUpperCase()}`; + const token = `FORGE_REAL_AGENT_SMOKE_${name.toUpperCase()}`; // Isolate spawned CLIs from host dotfiles/credentials unless explicitly testing // authenticated local agent profiles. const tempHome = RUN_REAL_AGENT_HOST_HOME ? undefined - : mkdtempSync(join(tmpdir(), 'parallel-code-test-home-')); + : mkdtempSync(join(tmpdir(), 'forge-test-home-')); const origHome = process.env.HOME; if (tempHome) { process.env.HOME = tempHome; @@ -185,7 +185,7 @@ describeRealAgents('Coordinator real agent startup smoke', () => { const task = await coordinator.createTask({ name: `${name} real startup smoke`, prompt: [ - 'This is a Parallel Code startup delivery smoke test.', + 'This is a Forge startup delivery smoke test.', 'Do not edit files, run commands, commit, or call tools.', `Reply with exactly this token and no extra text: ${token}`, ].join(' '), diff --git a/electron/mcp/coordinator-real-pty.integration.test.ts b/electron/mcp/coordinator-real-pty.integration.test.ts index c8a175ae..c3eac476 100644 --- a/electron/mcp/coordinator-real-pty.integration.test.ts +++ b/electron/mcp/coordinator-real-pty.integration.test.ts @@ -35,11 +35,11 @@ function runGit(cwd: string, args: string[]): void { } function createRepo(): string { - const repo = mkdtempSync(join(tmpdir(), 'parallel-code-real-pty-repo-')); + const repo = mkdtempSync(join(tmpdir(), 'forge-real-pty-repo-')); runGit(repo, ['init']); runGit(repo, ['checkout', '-b', 'main']); - runGit(repo, ['config', 'user.email', 'parallel-code-test@example.com']); - runGit(repo, ['config', 'user.name', 'Parallel Code Test']); + runGit(repo, ['config', 'user.email', 'forge-test@example.com']); + runGit(repo, ['config', 'user.name', 'Forge Test']); writeFileSync(join(repo, 'README.md'), '# test repo\n'); runGit(repo, ['add', 'README.md']); runGit(repo, ['commit', '-m', 'initial']); diff --git a/electron/mcp/coordinator.test.ts b/electron/mcp/coordinator.test.ts index 3b7c3431..ae389861 100644 --- a/electron/mcp/coordinator.test.ts +++ b/electron/mcp/coordinator.test.ts @@ -788,7 +788,7 @@ describe('Coordinator registerCoordinator — idempotency', () => { coordinatorTaskId: 'coord-1', }); const output = mockSubscribeToAgent.mock.calls[0]?.[1] as (encoded: string) => void; - output(encode('Starting MCP servers (0/2): codex_apps, parallel-code\n›')); + output(encode('Starting MCP servers (0/2): codex_apps, forge\n›')); await vi.advanceTimersByTimeAsync(1_500); @@ -3196,7 +3196,7 @@ describe('Coordinator MCP_TaskCreated spawn settings', () => { }; expect(payload.mcpLaunchArgs).toEqual([ '--config', - expect.stringContaining('mcp_servers.parallel-code'), + expect.stringContaining('mcp_servers.forge'), ]); }); @@ -3265,15 +3265,15 @@ describe('Coordinator sub-task MCP config isolation', () => { await coordinator.createTask({ name: 'task-b', prompt: 'do b', coordinatorTaskId: 'coord-1' }); const configWrites = mockAtomicWriteFile.mock.calls.filter((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(configWrites).toHaveLength(2); const taskIds = configWrites.map((c) => { const cfg = JSON.parse(c[1] as string) as { - mcpServers: { 'parallel-code': { args: string[] } }; + mcpServers: { forge: { args: string[] } }; }; - const args = cfg.mcpServers['parallel-code'].args; + const args = cfg.mcpServers['forge'].args; const idx = args.indexOf('--task-id'); return idx >= 0 ? args[idx + 1] : null; }); @@ -3303,7 +3303,7 @@ describe('Coordinator sub-task MCP config isolation', () => { await coordinator.createTask({ name: 'task-b', prompt: 'do b', coordinatorTaskId: 'coord-1' }); const configPaths = mockAtomicWriteFile.mock.calls - .filter((c) => (c[0] as string).includes('parallel-code-subtask-')) + .filter((c) => (c[0] as string).includes('forge-subtask-')) .map((c) => c[0] as string); expect(configPaths[0]).not.toBe(configPaths[1]); @@ -3343,17 +3343,15 @@ describe('Coordinator MCP config restart rewrite', () => { expect(task?.mcpConfigPath).toBeDefined(); const initialWrite = mockAtomicWriteFile.mock.calls.find((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(initialWrite).toBeDefined(); if (!initialWrite) throw new Error('expected initial config write'); const initialConfig = JSON.parse(initialWrite[1] as string) as { - mcpServers: { 'parallel-code': { args: string[]; env: Record } }; + mcpServers: { forge: { args: string[]; env: Record } }; }; - expect(initialConfig.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe( - 'old-token', - ); - expect(initialConfig.mcpServers['parallel-code'].args).toContain('http://localhost:3001'); + expect(initialConfig.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('old-token'); + expect(initialConfig.mcpServers['forge'].args).toContain('http://localhost:3001'); // Simulate coordinator restart with new port/token mockAtomicWriteFileSync.mockClear(); @@ -3366,7 +3364,7 @@ describe('Coordinator MCP config restart rewrite', () => { ); const rewriteCall = mockAtomicWriteFileSync.mock.calls.find((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(rewriteCall).toBeDefined(); if (!rewriteCall) throw new Error('expected rewrite call'); @@ -3375,16 +3373,14 @@ describe('Coordinator MCP config restart rewrite', () => { // Rewritten config is valid JSON with updated URL and token, preserving the task id const newConfig = JSON.parse(rewriteCall[1] as string) as { - mcpServers: { 'parallel-code': { args: string[]; env: Record } }; + mcpServers: { forge: { args: string[]; env: Record } }; }; - const newArgs = newConfig.mcpServers['parallel-code'].args; - expect(newConfig.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe('new-token'); + const newArgs = newConfig.mcpServers['forge'].args; + expect(newConfig.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('new-token'); expect(newArgs).toContain('http://localhost:3002'); expect(newArgs).toContain('task-1'); // Old values must be gone - expect(newConfig.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).not.toBe( - 'old-token', - ); + expect(newConfig.mcpServers['forge'].env['FORGE_MCP_TOKEN']).not.toBe('old-token'); expect(newArgs).not.toContain('http://localhost:3001'); }); @@ -3406,7 +3402,7 @@ describe('Coordinator MCP config restart rewrite', () => { ); const configWrites = mockAtomicWriteFileSync.mock.calls.filter((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(configWrites).toHaveLength(0); }); @@ -3436,16 +3432,16 @@ describe('Coordinator MCP config restart rewrite', () => { ); const rewrites = mockAtomicWriteFileSync.mock.calls.filter((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(rewrites).toHaveLength(2); for (const rewrite of rewrites) { const cfg = JSON.parse(rewrite[1] as string) as { - mcpServers: { 'parallel-code': { args: string[]; env: Record } }; + mcpServers: { forge: { args: string[]; env: Record } }; }; - expect(cfg.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe('new-token'); - expect(cfg.mcpServers['parallel-code'].args).toContain('http://localhost:3002'); + expect(cfg.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('new-token'); + expect(cfg.mcpServers['forge'].args).toContain('http://localhost:3002'); } }); @@ -3464,7 +3460,7 @@ describe('Coordinator MCP config restart rewrite', () => { ); const configWrites = mockAtomicWriteFileSync.mock.calls.filter((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(configWrites).toHaveLength(0); }); @@ -3500,15 +3496,15 @@ describe('Coordinator two-class token — subtask configs use subtaskToken', () await coordinator.createTask({ name: 'test', prompt: 'do', coordinatorTaskId: 'coord-1' }); const configWrite = mockAtomicWriteFile.mock.calls.find((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(configWrite).toBeDefined(); if (!configWrite) throw new Error('expected config write'); const config = JSON.parse(configWrite[1] as string) as { - mcpServers: { 'parallel-code': { env: Record } }; + mcpServers: { forge: { env: Record } }; }; - const writtenToken = config.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']; + const writtenToken = config.mcpServers['forge'].env['FORGE_MCP_TOKEN']; expect(writtenToken).toBe('subtask-secret'); expect(writtenToken).not.toBe('coordinator-secret'); }); @@ -3533,15 +3529,15 @@ describe('Coordinator two-class token — subtask configs use subtaskToken', () ); const rewrite = mockAtomicWriteFileSync.mock.calls.find((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(rewrite).toBeDefined(); if (!rewrite) throw new Error('expected rewrite'); const config = JSON.parse(rewrite[1] as string) as { - mcpServers: { 'parallel-code': { env: Record } }; + mcpServers: { forge: { env: Record } }; }; - const writtenToken = config.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']; + const writtenToken = config.mcpServers['forge'].env['FORGE_MCP_TOKEN']; expect(writtenToken).toBe('new-subtask'); expect(writtenToken).not.toBe('new-coordinator'); }); @@ -4330,7 +4326,7 @@ describe('Coordinator cleanupTask — Docker sub-task container stop', () => { coordinator.setDefaultProject('proj-1', '/tmp/project'); coordinator.registerCoordinator('coord-1', 'proj-1'); coordinator.setDockerContainerName('coord-1', 'my-coord-container'); - coordinator.setDockerImage('coord-1', 'parallel-code-agent:latest'); + coordinator.setDockerImage('coord-1', 'forge-agent:latest'); }); it('closeTask calls killAgent (which stops the sub-task Docker container via pty.ts)', async () => { @@ -4480,11 +4476,11 @@ describe('Multiple Docker coordinators — isolation', () => { }); it('each coordinator has an isolated docker container name (no singleton leak)', () => { - coordA.setDockerContainerName('coord-a', 'parallel-code-container-a'); - coordB.setDockerContainerName('coord-b', 'parallel-code-container-b'); + coordA.setDockerContainerName('coord-a', 'forge-container-a'); + coordB.setDockerContainerName('coord-b', 'forge-container-b'); // The container names must differ — no shared singleton - expect('parallel-code-container-a').not.toBe('parallel-code-container-b'); + expect('forge-container-a').not.toBe('forge-container-b'); }); it('sub-task MCP config for coord-a uses coord-a coordinator id, not coord-b', async () => { @@ -4498,17 +4494,15 @@ describe('Multiple Docker coordinators — isolation', () => { await coordA.createTask({ name: 'task-a', prompt: 'do a', coordinatorTaskId: 'coord-a' }); const configWrites = mockAtomicWriteFile.mock.calls.filter((c) => - (c[0] as string).includes('parallel-code-subtask-'), + (c[0] as string).includes('forge-subtask-'), ); expect(configWrites).toHaveLength(1); const cfg = JSON.parse(configWrites[0][1] as string) as { - mcpServers: { 'parallel-code': { args: string[]; env: Record } }; + mcpServers: { forge: { args: string[]; env: Record } }; }; - expect(cfg.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe('subtask-tok-a'); - expect(cfg.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).not.toBe( - 'subtask-tok-b', - ); + expect(cfg.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('subtask-tok-a'); + expect(cfg.mcpServers['forge'].env['FORGE_MCP_TOKEN']).not.toBe('subtask-tok-b'); }); }); @@ -4530,7 +4524,7 @@ describe('Coordinator Docker sub-task — per-container spawn', () => { coordinator.setDefaultProject('proj-1', '/tmp/project'); coordinator.registerCoordinator('coord-1', 'proj-1'); coordinator.setDockerContainerName('coord-1', 'my-coord-container'); - coordinator.setDockerImage('coord-1', 'parallel-code-agent:latest'); + coordinator.setDockerImage('coord-1', 'forge-agent:latest'); }); it('sub-task spawned with dockerMode: true (docker run), not docker exec', async () => { @@ -4728,7 +4722,7 @@ describe('Coordinator close with active sub-tasks', () => { const task = coordinator.getTask('task-1'); const configPath = task?.mcpConfigPath; expect(configPath).toBeDefined(); - expect(configPath).toMatch(/parallel-code-subtask-task-1\.json$/); + expect(configPath).toMatch(/forge-subtask-task-1\.json$/); mockUnlinkSync.mockClear(); await coordinator.closeTask('task-1'); @@ -4747,11 +4741,11 @@ describe('Coordinator close with active sub-tasks', () => { mockUnlinkSync.mockClear(); await coordinator.closeTask('task-1'); - // unlinkSync should NOT have been called with a parallel-code path - const parallelCodeCalls = mockUnlinkSync.mock.calls.filter( - (c) => typeof c[0] === 'string' && (c[0] as string).includes('parallel-code-subtask'), + // unlinkSync should NOT have been called with a forge path + const forgeCalls = mockUnlinkSync.mock.calls.filter( + (c) => typeof c[0] === 'string' && (c[0] as string).includes('forge-subtask'), ); - expect(parallelCodeCalls).toHaveLength(0); + expect(forgeCalls).toHaveLength(0); }); it('task is removed from map even when docker exec sub-tasks are active', async () => { @@ -4938,10 +4932,10 @@ describe('Coordinator removeCoordinatedTask', () => { mockUnlinkSync.mockClear(); coordinator.removeCoordinatedTask('task-1'); - const parallelCodeCalls = mockUnlinkSync.mock.calls.filter( - (c) => typeof c[0] === 'string' && (c[0] as string).includes('parallel-code-subtask'), + const forgeCalls = mockUnlinkSync.mock.calls.filter( + (c) => typeof c[0] === 'string' && (c[0] as string).includes('forge-subtask'), ); - expect(parallelCodeCalls).toHaveLength(0); + expect(forgeCalls).toHaveLength(0); }); it('does NOT notify renderer (UI already removed the task)', async () => { @@ -5002,7 +4996,7 @@ describe('Coordinator restart round-trip integration', () => { ); const taskId = 'hydrated-restart-1'; - const configPath = join(os.tmpdir(), `parallel-code-subtask-${taskId}.json`); + const configPath = join(os.tmpdir(), `forge-subtask-${taskId}.json`); mockAtomicWriteFileSync.mockClear(); coordinator.hydrateTask({ @@ -5021,9 +5015,9 @@ describe('Coordinator restart round-trip integration', () => { expect(rewrite).toBeDefined(); if (!rewrite) throw new Error('expected config rewrite'); const config = JSON.parse(rewrite[1] as string) as { - mcpServers: { 'parallel-code': { env: Record } }; + mcpServers: { forge: { env: Record } }; }; - const writtenToken = config.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']; + const writtenToken = config.mcpServers['forge'].env['FORGE_MCP_TOKEN']; expect(writtenToken).toBe('new-subtask-secret'); expect(writtenToken).not.toBe('new-coordinator-secret'); }); @@ -5038,7 +5032,7 @@ describe('Coordinator restart round-trip integration', () => { ); const taskId = 'hydrated-restart-codex'; - const configPath = join(os.tmpdir(), `parallel-code-subtask-${taskId}.json`); + const configPath = join(os.tmpdir(), `forge-subtask-${taskId}.json`); const result = coordinator.hydrateTask({ id: taskId, @@ -5100,7 +5094,7 @@ describe('Coordinator restart round-trip integration', () => { ); const taskId = 'hydrated-restart-existing'; - const configPath = join(os.tmpdir(), `parallel-code-subtask-${taskId}.json`); + const configPath = join(os.tmpdir(), `forge-subtask-${taskId}.json`); const args = { id: taskId, name: 'hydrated-task', @@ -5134,7 +5128,7 @@ describe('Coordinator restart round-trip integration', () => { const taskId = 'hydrated-restart-2'; const agentId = 'agent-restart-2'; - const configPath = join(os.tmpdir(), `parallel-code-subtask-${taskId}.json`); + const configPath = join(os.tmpdir(), `forge-subtask-${taskId}.json`); coordinator.hydrateTask({ id: taskId, @@ -5184,7 +5178,7 @@ describe('Coordinator hydrateTask — mcpConfigPath directory scoping', () => { 'http://localhost:3001', 'tok', 'subtok', - '/srv/app/.parallel-code/mcp-server.js', + '/srv/app/.forge/mcp-server.js', ); }); @@ -5219,7 +5213,7 @@ describe('Coordinator hydrateTask — mcpConfigPath directory scoping', () => { worktreePath: '/tmp/wrong-dir', agentId: 'agent-wrong-dir', coordinatorTaskId: 'coord-1', - mcpConfigPath: `/tmp/evil/parallel-code-subtask-${taskId}.json`, + mcpConfigPath: `/tmp/evil/forge-subtask-${taskId}.json`, }); expect(coordinator.getTask(taskId)?.mcpConfigPath).toBeUndefined(); @@ -5227,7 +5221,7 @@ describe('Coordinator hydrateTask — mcpConfigPath directory scoping', () => { it('correct host tmpdir path is accepted and config write occurs', () => { const taskId = 'task-valid-host'; - const validPath = join(os.tmpdir(), `parallel-code-subtask-${taskId}.json`); + const validPath = join(os.tmpdir(), `forge-subtask-${taskId}.json`); mockAtomicWriteFileSync.mockClear(); coordinator.hydrateTask({ @@ -5249,7 +5243,7 @@ describe('Coordinator hydrateTask — mcpConfigPath directory scoping', () => { it('Docker mode: dirname(serverPath)/subtask-{id}.json is accepted and config write occurs', () => { const taskId = 'task-valid-docker'; - const serverPath = '/srv/app/.parallel-code/mcp-server.js'; + const serverPath = '/srv/app/.forge/mcp-server.js'; const dockerPath = join(dirname(serverPath), `subtask-${taskId}.json`); mockAtomicWriteFileSync.mockClear(); @@ -5361,8 +5355,8 @@ describe('Coordinator Docker mode — per-container sub-tasks', () => { coordinator.registerCoordinator('coord-docker', 'proj-1', { worktreePath: '/tmp/project/.worktrees/task/coord', }); - coordinator.setDockerContainerName('coord-docker', 'parallel-code-abcdef123456'); - coordinator.setDockerImage('coord-docker', 'parallel-code-agent:latest'); + coordinator.setDockerContainerName('coord-docker', 'forge-abcdef123456'); + coordinator.setDockerImage('coord-docker', 'forge-agent:latest'); coordinator.setCoordinatorSpawnDefaults('coord-docker', 'claude', []); }); @@ -5382,7 +5376,7 @@ describe('Coordinator Docker mode — per-container sub-tasks', () => { // Args do not contain 'exec' expect(spawnCall.args).not.toContain('exec'); // Args do not contain the coordinator container name - expect(spawnCall.args).not.toContain('parallel-code-abcdef123456'); + expect(spawnCall.args).not.toContain('forge-abcdef123456'); }); it('createTask passes the coordinator Docker image to spawnAgent', async () => { @@ -5392,10 +5386,10 @@ describe('Coordinator Docker mode — per-container sub-tasks', () => { }); const spawnCall = mockSpawnAgent.mock.calls[0][1]; - expect(spawnCall.dockerImage).toBe('parallel-code-agent:latest'); + expect(spawnCall.dockerImage).toBe('forge-agent:latest'); }); - it('createTask sets dockerMountWorktreeParent: true so coordinator .parallel-code/ is accessible', async () => { + it('createTask sets dockerMountWorktreeParent: true so coordinator .forge/ is accessible', async () => { await coordinator.createTask({ name: 'docker-sub-task', coordinatorTaskId: 'coord-docker', @@ -5854,13 +5848,13 @@ describe('Coordinator deregisterCoordinator — .mcp.json cleanup', () => { coordinator.setMcpJsonInfo('coord-1', '/tmp/.mcp.json', false); }); - it('removes only the parallel-code key and preserves other servers', () => { + it('removes only the forge key and preserves other servers', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue( JSON.stringify({ mcpServers: { 'my-server': { command: 'my-tool', args: [] }, - 'parallel-code': { command: 'node', args: ['server.js'] }, + forge: { command: 'node', args: ['server.js'] }, }, }), ); @@ -5875,15 +5869,15 @@ describe('Coordinator deregisterCoordinator — .mcp.json cleanup', () => { mcpServers: Record; }; expect(written.mcpServers['my-server']).toBeDefined(); - expect(written.mcpServers['parallel-code']).toBeUndefined(); + expect(written.mcpServers['forge']).toBeUndefined(); expect(mockUnlinkSync).not.toHaveBeenCalledWith('/tmp/.mcp.json'); }); - it('deletes the file when parallel-code was the only entry', () => { + it('deletes the file when forge was the only entry', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue( JSON.stringify({ - mcpServers: { 'parallel-code': { command: 'node', args: ['server.js'] } }, + mcpServers: { forge: { command: 'node', args: ['server.js'] } }, }), ); @@ -5909,7 +5903,7 @@ describe('Coordinator deregisterCoordinator — .mcp.json cleanup', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue( JSON.stringify({ - mcpServers: { 'parallel-code': { command: 'node', args: [] } }, + mcpServers: { forge: { command: 'node', args: [] } }, someOtherKey: 'kept', }), ); diff --git a/electron/mcp/coordinator.ts b/electron/mcp/coordinator.ts index 5b4132d2..7ff93fe9 100644 --- a/electron/mcp/coordinator.ts +++ b/electron/mcp/coordinator.ts @@ -138,7 +138,7 @@ function execStdout(result: Awaited>): string { } /** - * The per-sub-task MCP config: a stdio launch of the parallel-code MCP server + * The per-sub-task MCP config: a stdio launch of the forge MCP server * scoped to one task. Identical across createTask, coordinator re-registration, * and hydration rewrite — only the resolved doneToken differs, so callers own * token generation and pass the final value in. @@ -152,13 +152,13 @@ function buildSubtaskMcpConfig(args: { }) { return { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio' as const, command: 'node', args: [args.serverPath, '--url', args.serverUrl, '--task-id', args.taskId], env: { - PARALLEL_CODE_MCP_TOKEN: args.subtaskToken, - PARALLEL_CODE_MCP_DONE_TOKEN: args.doneToken, + FORGE_MCP_TOKEN: args.subtaskToken, + FORGE_MCP_DONE_TOKEN: args.doneToken, }, }, }, @@ -963,7 +963,7 @@ export class Coordinator { } task.preambleFileExistedBefore = preambleFileOriginalContent !== null; // Write a per-sub-task MCP config so the agent can call signal_done. - // In Docker mode, write to the coordinator's .parallel-code/ dir (which IS the explicitly + // In Docker mode, write to the coordinator's .forge/ dir (which IS the explicitly // mounted volume) rather than the sub-task worktree (which may not be in the container). // Always pass explicit MCP launch args so agents don't rely on auto-discovery. const mcpServerInfoForTask = coordinatorState.mcpServerInfo; @@ -1016,7 +1016,7 @@ export class Coordinator { dockerMode: true, dockerImage: coordinatorState.dockerImage ?? undefined, // Mount parent dir so the sub-task can reach the coordinator's - // .parallel-code/ dir (which holds the per-sub-task MCP config). + // .forge/ dir (which holds the per-sub-task MCP config). // resolveWorktreeGitDirMount adds the main .git dir mount. dockerMountWorktreeParent: true, } @@ -1504,7 +1504,7 @@ export class Coordinator { } await execAsync('git', ['add', '-A', '--', ...dirtyPaths], { cwd: task.worktreePath }); try { - await execAsync('git', ['commit', '-m', 'Remove Parallel Code sub-task preamble'], { + await execAsync('git', ['commit', '-m', 'Remove Forge sub-task preamble'], { cwd: task.worktreePath, }); } catch { @@ -1984,10 +1984,10 @@ export class Coordinator { // Validate the persisted mcpConfigPath is exactly one of the two paths that // getSubTaskMcpConfigPath generates — basename-only is too permissive and would // allow a crafted state file to direct the token write to an arbitrary location. - // Host mode: os.tmpdir()/parallel-code-subtask-{id}.json + // Host mode: os.tmpdir()/forge-subtask-{id}.json // Docker mode: dirname(serverPath)/subtask-{id}.json (looked up from live coordinator state) const serverInfo = this.coordinators.get(opts.coordinatorTaskId)?.mcpServerInfo; - const expectedHostPath = join(os.tmpdir(), `parallel-code-subtask-${opts.id}.json`); + const expectedHostPath = join(os.tmpdir(), `forge-subtask-${opts.id}.json`); const expectedDockerPath = serverInfo ? join(dirname(serverInfo.serverPath), `subtask-${opts.id}.json`) : null; @@ -2148,15 +2148,15 @@ export class Coordinator { coordinatorTaskId: string, mcpJsonPath: string, createdMcpJson: boolean, - previousMcpParallelCode?: unknown, - writtenMcpParallelCode?: unknown, + previousMcpForge?: unknown, + writtenMcpForge?: unknown, ): void { const state = this.coordinators.get(coordinatorTaskId); if (state) { state.mcpJsonPath = mcpJsonPath; state.createdMcpJson = createdMcpJson; - state.previousMcpParallelCode = previousMcpParallelCode; - state.writtenMcpParallelCode = writtenMcpParallelCode; + state.previousMcpForge = previousMcpForge; + state.writtenMcpForge = writtenMcpForge; } } @@ -2176,9 +2176,9 @@ export class Coordinator { }); } - // Clean up coordinator .mcp.json — restore or remove only the parallel-code key. + // Clean up coordinator .mcp.json — restore or remove only the forge key. // Always read current contents (user may have added keys while running). - // If there was a pre-existing parallel-code entry, restore it; otherwise delete the key. + // If there was a pre-existing forge entry, restore it; otherwise delete the key. if (coordinator.mcpJsonPath) { try { const raw = existsSync(coordinator.mcpJsonPath) @@ -2187,17 +2187,17 @@ export class Coordinator { if (raw !== null) { const content = JSON.parse(raw) as { mcpServers?: Record }; if (content.mcpServers) { - const current = content.mcpServers['parallel-code']; - const weWrote = coordinator.writtenMcpParallelCode; + const current = content.mcpServers['forge']; + const weWrote = coordinator.writtenMcpForge; // Only restore/delete if the current value still matches what we wrote, // or if we don't have a record of what we wrote (legacy path — always restore). const safeToRestore = weWrote === undefined || JSON.stringify(current) === JSON.stringify(weWrote); if (safeToRestore) { - if (coordinator.previousMcpParallelCode !== undefined) { - content.mcpServers['parallel-code'] = coordinator.previousMcpParallelCode; + if (coordinator.previousMcpForge !== undefined) { + content.mcpServers['forge'] = coordinator.previousMcpForge; } else { - delete content.mcpServers['parallel-code']; + delete content.mcpServers['forge']; } } } diff --git a/electron/mcp/docker.integration.test.ts b/electron/mcp/docker.integration.test.ts index 5d8ce020..097b9d0e 100644 --- a/electron/mcp/docker.integration.test.ts +++ b/electron/mcp/docker.integration.test.ts @@ -69,9 +69,9 @@ describeDocker('Docker MCP integration', () => { if (worktreePath) rmSync(worktreePath, { recursive: true, force: true }); }); - it('runs the bundled MCP server inside the Parallel Code Docker image and reaches the host remote API', async () => { - worktreePath = mkdtempSync(join(tmpdir(), 'parallel-code-docker-mcp-')); - const mcpDir = join(worktreePath, '.parallel-code'); + it('runs the bundled MCP server inside the Forge Docker image and reaches the host remote API', async () => { + worktreePath = mkdtempSync(join(tmpdir(), 'forge-docker-mcp-')); + const mcpDir = join(worktreePath, '.forge'); mkdirSync(mcpDir, { recursive: true }); const bundledMcpServerPath = join(process.cwd(), 'dist-electron', 'mcp-server.cjs'); @@ -115,14 +115,14 @@ describeDocker('Docker MCP integration', () => { getCoordinator: () => coordinator, }); - const serverUrl = getMCPRemoteServerUrl(remoteServer.port, 'parallel-code-test-container'); + const serverUrl = getMCPRemoteServerUrl(remoteServer.port, 'forge-test-container'); const mcpConfig = { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio', command: 'node', args: [dockerMcpServerPath, '--url', serverUrl, '--coordinator-id', 'coord-1'], - env: { PARALLEL_CODE_MCP_TOKEN: remoteServer.token }, + env: { FORGE_MCP_TOKEN: remoteServer.token }, }, }, }; @@ -141,7 +141,7 @@ describeDocker('Docker MCP integration', () => { '-w', worktreePath, '-e', - `PARALLEL_CODE_MCP_TOKEN=${remoteServer.token}`, + `FORGE_MCP_TOKEN=${remoteServer.token}`, DOCKER_DEFAULT_IMAGE, 'node', dockerMcpServerPath, @@ -151,7 +151,7 @@ describeDocker('Docker MCP integration', () => { 'coord-1', ], }); - const client = new Client({ name: 'parallel-code-docker-mcp-test', version: '1.0.0' }); + const client = new Client({ name: 'forge-docker-mcp-test', version: '1.0.0' }); try { await client.connect(transport); @@ -174,9 +174,9 @@ describeDocker('Docker MCP integration', () => { // Regression test for the bug where sub-task MCP config was written to the sub-task // worktree (not a Docker volume mount) and relied on auto-discovery instead of --mcp-config. // This test simulates a sub-task running via `docker exec --mcp-config `. - it('sub-task reaches MCP server via explicit --mcp-config in coordinator .parallel-code dir', async () => { - const coordWorktree = mkdtempSync(join(tmpdir(), 'parallel-code-coord-')); - const mcpDir = join(coordWorktree, '.parallel-code'); + it('sub-task reaches MCP server via explicit --mcp-config in coordinator .forge dir', async () => { + const coordWorktree = mkdtempSync(join(tmpdir(), 'forge-coord-')); + const mcpDir = join(coordWorktree, '.forge'); mkdirSync(mcpDir, { recursive: true }); const bundledMcpServerPath = join(process.cwd(), 'dist-electron', 'mcp-server.cjs'); @@ -212,28 +212,28 @@ describeDocker('Docker MCP integration', () => { }); try { - const serverUrl = getMCPRemoteServerUrl(port2, 'parallel-code-test-container'); + const serverUrl = getMCPRemoteServerUrl(port2, 'forge-test-container'); const taskId = 'sub-task-docker-test'; - // getSubTaskMcpConfigPath must use the coordinator's .parallel-code dir, not a sub-task worktree + // getSubTaskMcpConfigPath must use the coordinator's .forge dir, not a sub-task worktree const configPath = getSubTaskMcpConfigPath( - 'parallel-code-test-container', + 'forge-test-container', dockerMcpServerPath, taskId, ); expect(dirname(configPath)).toBe(mcpDir); // the coordinator's volume dir - // Assert the config path is inside the coordinator's .parallel-code dir (which IS a Docker volume) + // Assert the config path is inside the coordinator's .forge dir (which IS a Docker volume) // NOT in any sub-task worktree (which is NOT a Docker volume) - expect(dirname(configPath).endsWith('/.parallel-code')).toBe(true); + expect(dirname(configPath).endsWith('/.forge')).toBe(true); expect(configPath.startsWith(coordWorktree)).toBe(true); const subMcpConfig = { mcpServers: { - 'parallel-code': { + forge: { type: 'stdio', command: 'node', args: [dockerMcpServerPath, '--url', serverUrl, '--task-id', taskId], - env: { PARALLEL_CODE_MCP_TOKEN: subServer.token }, + env: { FORGE_MCP_TOKEN: subServer.token }, }, }, }; @@ -252,7 +252,7 @@ describeDocker('Docker MCP integration', () => { '-w', coordWorktree, '-e', - `PARALLEL_CODE_MCP_TOKEN=${subServer.token}`, + `FORGE_MCP_TOKEN=${subServer.token}`, DOCKER_DEFAULT_IMAGE, 'node', dockerMcpServerPath, @@ -262,7 +262,7 @@ describeDocker('Docker MCP integration', () => { taskId, ], }); - const subClient = new Client({ name: 'parallel-code-subtask-test', version: '1.0.0' }); + const subClient = new Client({ name: 'forge-subtask-test', version: '1.0.0' }); try { await subClient.connect(transport); @@ -295,7 +295,7 @@ describeDocker('Layer 2 — Docker smoke tests', () => { beforeAll(async () => { requireDockerImage(DOCKER_DEFAULT_IMAGE); - coordWorktree = mkdtempSync(join(tmpdir(), 'parallel-code-smoke-')); + coordWorktree = mkdtempSync(join(tmpdir(), 'forge-smoke-')); remotePort = await findFreePort(); const mockCoordinator = { listTasks: () => [] } as unknown as Coordinator; @@ -437,7 +437,7 @@ describeDocker('Layer 4 — Production-path coordinator Docker scenario', () => }); it('uses production config pipeline to generate .mcp.json, then create_task reaches coordinator', async () => { - scenarioWorktree = mkdtempSync(join(tmpdir(), 'parallel-code-prod-path-')); + scenarioWorktree = mkdtempSync(join(tmpdir(), 'forge-prod-path-')); // --- Start remote server with a real coordinator stub --- const port = await findFreePort(); @@ -490,7 +490,7 @@ describeDocker('Layer 4 — Production-path coordinator Docker scenario', () => writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 }); // --- Assert the generated config uses the correct URL --- - const args = mcpConfig.mcpServers['parallel-code'].args; + const args = mcpConfig.mcpServers['forge'].args; const urlIdx = args.indexOf('--url'); if (platform() === 'darwin') { expect(args[urlIdx + 1]).toContain('host.docker.internal'); @@ -515,7 +515,7 @@ describeDocker('Layer 4 — Production-path coordinator Docker scenario', () => '-w', scenarioWorktree, '-e', - `PARALLEL_CODE_MCP_TOKEN=${scenarioServer.token}`, + `FORGE_MCP_TOKEN=${scenarioServer.token}`, DOCKER_DEFAULT_IMAGE, 'node', destPath, @@ -525,7 +525,7 @@ describeDocker('Layer 4 — Production-path coordinator Docker scenario', () => 'coord-prod', ], }); - const client = new Client({ name: 'parallel-code-prod-path-test', version: '1.0.0' }); + const client = new Client({ name: 'forge-prod-path-test', version: '1.0.0' }); try { await client.connect(transport); @@ -552,7 +552,7 @@ describeDocker('Layer 4 — Production-path coordinator Docker scenario', () => // // No Docker required. Verifies that StartMCPServer args accept `dockerImage` // and that `getSubTaskMcpConfigPath` still returns a path in the coordinator's -// .parallel-code/ dir (the only dir that is a mounted volume in both coordinator +// .forge/ dir (the only dir that is a mounted volume in both coordinator // and per-sub-task containers). describe('Docker per-container sub-tasks — StartMCPServer arg validation', () => { @@ -564,8 +564,8 @@ describe('Docker per-container sub-tasks — StartMCPServer arg validation', () coordinatorTaskId: VALID_UUID, projectId: 'proj-1', projectRoot: '/tmp/project', - dockerContainerName: 'parallel-code-abc123', - dockerImage: 'parallel-code-agent:latest', + dockerContainerName: 'forge-abc123', + dockerImage: 'forge-agent:latest', }), ).not.toThrow(); }); @@ -581,12 +581,12 @@ describe('Docker per-container sub-tasks — StartMCPServer arg validation', () ).toThrow('dockerImage must not be blank'); }); - it('sub-task MCP config path is in coordinator .parallel-code/ dir (a Docker volume), not the sub-task worktree', () => { + it('sub-task MCP config path is in coordinator .forge/ dir (a Docker volume), not the sub-task worktree', () => { const coordWorktree = '/tmp/project/.worktrees/task/coord-abc'; - const mcpServerPath = `${coordWorktree}/.parallel-code/mcp-server.cjs`; - const configPath = getSubTaskMcpConfigPath('parallel-code-coord-abc', mcpServerPath, 'sub-1'); - // Must be inside the coordinator's .parallel-code/ dir - expect(configPath.startsWith(`${coordWorktree}/.parallel-code/`)).toBe(true); + const mcpServerPath = `${coordWorktree}/.forge/mcp-server.cjs`; + const configPath = getSubTaskMcpConfigPath('forge-coord-abc', mcpServerPath, 'sub-1'); + // Must be inside the coordinator's .forge/ dir + expect(configPath.startsWith(`${coordWorktree}/.forge/`)).toBe(true); // Must NOT be in a sub-task worktree (which is not a Docker volume) expect(configPath).not.toContain('.worktrees/task/sub-'); }); @@ -616,7 +616,7 @@ describe('Docker coordinator bootstrap — path edge cases', () => { it('preserves spaces in worktree path for mcp-server.cjs dest', () => { const worktreePath = '/Users/alice bob/my repos/.worktrees/task/coord-abc'; const dest = getDockerMcpServerDestPath(worktreePath, '/irrelevant'); - expect(dest).toBe(`${worktreePath}/.parallel-code/mcp-server.cjs`); + expect(dest).toBe(`${worktreePath}/.forge/mcp-server.cjs`); }); it('preserves spaces in worktree path for .mcp.json dir selection', () => { @@ -630,15 +630,13 @@ describe('Docker coordinator bootstrap — path edge cases', () => { // Separately, the worktreePath IS the Claude trust key — verify it is not mangled. const worktreePath = '/Users/alice bob/my repos/.worktrees/task/coord abc 123'; const cfg = buildCoordinatorMCPConfig({ - mcpServerPath: `${worktreePath}/.parallel-code/mcp-server.cjs`, + mcpServerPath: `${worktreePath}/.forge/mcp-server.cjs`, serverUrl: 'http://host.docker.internal:3001', token: 'tok', coordinatorTaskId: 'coord-1', }); // The MCP server path arg must contain the verbatim worktree path - expect(cfg.mcpServers['parallel-code'].args[0]).toBe( - `${worktreePath}/.parallel-code/mcp-server.cjs`, - ); + expect(cfg.mcpServers['forge'].args[0]).toBe(`${worktreePath}/.forge/mcp-server.cjs`); }); }); @@ -647,15 +645,15 @@ describe('Docker coordinator bootstrap — path edge cases', () => { describe('Docker coordinator bootstrap — port/token rotation in .mcp.json', () => { it('buildCoordinatorMCPConfig reflects new port when called with updated serverUrl', () => { const opts = { - mcpServerPath: '/worktrees/coord/.parallel-code/mcp-server.cjs', + mcpServerPath: '/worktrees/coord/.forge/mcp-server.cjs', serverUrl: 'http://host.docker.internal:3001', token: 'old-token', coordinatorTaskId: 'coord-1', }; const cfgBefore = buildCoordinatorMCPConfig(opts); - const argsBefore = cfgBefore.mcpServers['parallel-code'].args; + const argsBefore = cfgBefore.mcpServers['forge'].args; expect(argsBefore).toContain('http://host.docker.internal:3001'); - expect(cfgBefore.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe('old-token'); + expect(cfgBefore.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('old-token'); // Simulate restart: new port and token const cfgAfter = buildCoordinatorMCPConfig({ @@ -663,13 +661,11 @@ describe('Docker coordinator bootstrap — port/token rotation in .mcp.json', () serverUrl: 'http://host.docker.internal:3099', token: 'new-token', }); - const argsAfter = cfgAfter.mcpServers['parallel-code'].args; + const argsAfter = cfgAfter.mcpServers['forge'].args; expect(argsAfter).toContain('http://host.docker.internal:3099'); - expect(cfgAfter.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).toBe('new-token'); + expect(cfgAfter.mcpServers['forge'].env['FORGE_MCP_TOKEN']).toBe('new-token'); expect(argsAfter).not.toContain('http://host.docker.internal:3001'); - expect(cfgAfter.mcpServers['parallel-code'].env['PARALLEL_CODE_MCP_TOKEN']).not.toBe( - 'old-token', - ); + expect(cfgAfter.mcpServers['forge'].env['FORGE_MCP_TOKEN']).not.toBe('old-token'); }); }); @@ -680,14 +676,14 @@ describe('Docker coordinator bootstrap — non-Claude agent MCP connectivity', ( // MCP config is written by the coordinator bootstrap, not by the inner agent. // A Codex or OpenCode agent in Docker still gets MCP injected via .mcp.json. const cfg = buildCoordinatorMCPConfig({ - mcpServerPath: '/worktrees/coord/.parallel-code/mcp-server.cjs', + mcpServerPath: '/worktrees/coord/.forge/mcp-server.cjs', serverUrl: 'http://host.docker.internal:3001', token: 'tok', coordinatorTaskId: 'coord-1', }); // Config structure is the same regardless of inner agent command - expect(cfg.mcpServers['parallel-code'].type).toBe('stdio'); - expect(cfg.mcpServers['parallel-code'].command).toBe('node'); + expect(cfg.mcpServers['forge'].type).toBe('stdio'); + expect(cfg.mcpServers['forge'].command).toBe('node'); }); }); @@ -739,7 +735,7 @@ describeDocker('Layer 9 — Project Dockerfile image MCP smoke test', () => { requireDockerImage(projectImage); const worktree = mkdtempSync(join(tmpdir(), 'docker-project-img-')); - const mcpDir = join(worktree, '.parallel-code'); + const mcpDir = join(worktree, '.forge'); mkdirSync(mcpDir, { recursive: true }); const mcpServerSrc = join(dirname(new URL(import.meta.url).pathname), '..', 'mcp-server.cjs'); @@ -769,7 +765,7 @@ describeDocker('Layer 9 — Project Dockerfile image MCP smoke test', () => { '-w', worktree, '-e', - `PARALLEL_CODE_MCP_TOKEN=${server.token}`, + `FORGE_MCP_TOKEN=${server.token}`, projectImage, 'node', destPath, @@ -799,7 +795,7 @@ describe('Docker coordinator bootstrap — unicode paths', () => { it('unicode characters in worktree path are preserved verbatim in config', () => { const worktreePath = '/Users/张三/projects/我的代码/.worktrees/task/coord-abc'; const dest = getDockerMcpServerDestPath(worktreePath, '/irrelevant'); - expect(dest).toBe(`${worktreePath}/.parallel-code/mcp-server.cjs`); + expect(dest).toBe(`${worktreePath}/.forge/mcp-server.cjs`); expect(selectMcpJsonDir(worktreePath, '/irrelevant')).toBe(worktreePath); }); @@ -810,21 +806,21 @@ describe('Docker coordinator bootstrap — unicode paths', () => { token: 'tok', coordinatorTaskId: 'coord-任务-1', }); - const coordIdx = cfg.mcpServers['parallel-code'].args.indexOf('--coordinator-id'); - expect(cfg.mcpServers['parallel-code'].args[coordIdx + 1]).toBe('coord-任务-1'); + const coordIdx = cfg.mcpServers['forge'].args.indexOf('--coordinator-id'); + expect(cfg.mcpServers['forge'].args[coordIdx + 1]).toBe('coord-任务-1'); }); it('unicode in worktree path generates valid JSON', () => { const worktreePath = '/home/ünïcödé/project/.worktrees/tâsk/coord'; const cfg = buildCoordinatorMCPConfig({ - mcpServerPath: `${worktreePath}/.parallel-code/mcp-server.cjs`, + mcpServerPath: `${worktreePath}/.forge/mcp-server.cjs`, serverUrl: 'http://host.docker.internal:3001', token: 'tök', coordinatorTaskId: 'coord-ünïcödé', }); const json = JSON.stringify(cfg, null, 2); const parsed = JSON.parse(json) as typeof cfg; - expect(parsed.mcpServers['parallel-code'].args[0]).toContain('ünïcödé'); + expect(parsed.mcpServers['forge'].args[0]).toContain('ünïcödé'); }); }); @@ -835,7 +831,7 @@ describeDocker('Layer 2 — MCP tool schema drift between host and Docker', () = requireDockerImage(DOCKER_DEFAULT_IMAGE); const worktree = mkdtempSync(join(tmpdir(), 'docker-schema-drift-')); - const mcpDir = join(worktree, '.parallel-code'); + const mcpDir = join(worktree, '.forge'); mkdirSync(mcpDir, { recursive: true }); const bundledMcpServerPath = join(process.cwd(), 'dist-electron', 'mcp-server.cjs'); @@ -859,7 +855,7 @@ describeDocker('Layer 2 — MCP tool schema drift between host and Docker', () = const hostTransport = new HostTransport({ command: 'node', args: [destPath, '--url', `http://127.0.0.1:${port}`], - env: { ...process.env, PARALLEL_CODE_MCP_TOKEN: server.token }, + env: { ...process.env, FORGE_MCP_TOKEN: server.token }, }); const hostClient = new HostClient({ name: 'host-schema-test', version: '1.0.0' }); @@ -882,7 +878,7 @@ describeDocker('Layer 2 — MCP tool schema drift between host and Docker', () = '-w', worktree, '-e', - `PARALLEL_CODE_MCP_TOKEN=${server.token}`, + `FORGE_MCP_TOKEN=${server.token}`, DOCKER_DEFAULT_IMAGE, 'node', destPath, @@ -929,7 +925,7 @@ describeDocker('Layer 2 — Large MCP response over Docker stdio', () => { requireDockerImage(DOCKER_DEFAULT_IMAGE); const worktree = mkdtempSync(join(tmpdir(), 'docker-large-resp-')); - const mcpDir = join(worktree, '.parallel-code'); + const mcpDir = join(worktree, '.forge'); mkdirSync(mcpDir, { recursive: true }); const bundledMcpServerPath = join(process.cwd(), 'dist-electron', 'mcp-server.cjs'); @@ -972,7 +968,7 @@ describeDocker('Layer 2 — Large MCP response over Docker stdio', () => { '-w', worktree, '-e', - `PARALLEL_CODE_MCP_TOKEN=${server.token}`, + `FORGE_MCP_TOKEN=${server.token}`, DOCKER_DEFAULT_IMAGE, 'node', destPath, diff --git a/electron/mcp/mcp-tool-list.ts b/electron/mcp/mcp-tool-list.ts index d820f4b9..2550a97d 100644 --- a/electron/mcp/mcp-tool-list.ts +++ b/electron/mcp/mcp-tool-list.ts @@ -10,7 +10,7 @@ export const SUBTASK_TOOLS: ToolDef[] = [ { name: 'land_self', description: - 'Land your own completed sub-task through the Parallel Code backend. Call this only after committing your work and running verification successfully. A successful call is terminal; do not call signal_done afterward.', + 'Land your own completed sub-task through the Forge backend. Call this only after committing your work and running verification successfully. A successful call is terminal; do not call signal_done afterward.', inputSchema: { type: 'object', properties: { diff --git a/electron/mcp/preamble.ts b/electron/mcp/preamble.ts index f5a270f7..6243c830 100644 --- a/electron/mcp/preamble.ts +++ b/electron/mcp/preamble.ts @@ -124,8 +124,8 @@ export async function buildNormalizedPreambleFileDiff( if (normalizedContent === baseContent) return ''; const id = randomUUID(); - const tmpBase = join(os.tmpdir(), `parallel-code-base-${id}`); - const tmpNorm = join(os.tmpdir(), `parallel-code-norm-${id}`); + const tmpBase = join(os.tmpdir(), `forge-base-${id}`); + const tmpNorm = join(os.tmpdir(), `forge-norm-${id}`); try { writeFileSync(tmpBase, baseContent); writeFileSync(tmpNorm, normalizedContent); diff --git a/electron/mcp/prompt-detect.test.ts b/electron/mcp/prompt-detect.test.ts index 159ee21d..0db9a1eb 100644 --- a/electron/mcp/prompt-detect.test.ts +++ b/electron/mcp/prompt-detect.test.ts @@ -145,9 +145,7 @@ describe('chunkContainsAgentPrompt', () => { ], [ 'Gemini prompt after MCP startup completion scrollback', - ['Starting MCP servers (0/1): parallel-code', 'Starting MCP servers complete', '>'].join( - '\n', - ), + ['Starting MCP servers (0/1): forge', 'Starting MCP servers complete', '>'].join('\n'), ], ])('classifies ready agent fixture: %s', (_name, fixture) => { expect(getAgentPromptReadiness(fixture)).toMatchObject({ ready: true, reason: 'ready' }); @@ -166,7 +164,7 @@ describe('chunkContainsAgentPrompt', () => { ], [ 'Codex startup screen', - 'Starting MCP servers (0/2): codex_apps, parallel-code\n›', + 'Starting MCP servers (0/2): codex_apps, forge\n›', 'startup_or_dialog', ], [ @@ -260,9 +258,9 @@ describe('chunkContainsAgentPrompt', () => { }); it('does not treat Codex startup screens as ready', () => { - expect( - chunkContainsAgentPrompt('Starting MCP servers (0/2): codex_apps, parallel-code\n›'), - ).toBe(false); + expect(chunkContainsAgentPrompt('Starting MCP servers (0/2): codex_apps, forge\n›')).toBe( + false, + ); expect(chunkContainsAgentPrompt('model: loading /model to change\n›')).toBe(false); }); diff --git a/electron/mcp/server.ts b/electron/mcp/server.ts index 29f7ac35..2c32239c 100644 --- a/electron/mcp/server.ts +++ b/electron/mcp/server.ts @@ -265,13 +265,13 @@ function parseArgs(argv: string[]): { url: string; taskId: string; coordinatorId async function main(): Promise { const { url, taskId, coordinatorId } = parseArgs(process.argv.slice(2)); - const token = process.env.PARALLEL_CODE_MCP_TOKEN ?? ''; - const doneToken = process.env.PARALLEL_CODE_MCP_DONE_TOKEN || undefined; + const token = process.env.FORGE_MCP_TOKEN ?? ''; + const doneToken = process.env.FORGE_MCP_DONE_TOKEN || undefined; if (!url || !token) { console.error( 'Usage: node server.js --url [--task-id ] [--coordinator-id ]\n' + - 'Token must be set via PARALLEL_CODE_MCP_TOKEN environment variable.', + 'Token must be set via FORGE_MCP_TOKEN environment variable.', ); process.exit(1); } @@ -289,10 +289,7 @@ async function main(): Promise { } const client = new MCPClient(url, token, coordinatorId || undefined, doneToken); - const server = new Server( - { name: 'parallel-code', version: '1.0.0' }, - { capabilities: { tools: {} } }, - ); + const server = new Server({ name: 'forge', version: '1.0.0' }, { capabilities: { tools: {} } }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: selectTools(taskId, coordinatorId) }; diff --git a/electron/mcp/sub-task-preamble.ts b/electron/mcp/sub-task-preamble.ts index dd70a19e..b7b91141 100644 --- a/electron/mcp/sub-task-preamble.ts +++ b/electron/mcp/sub-task-preamble.ts @@ -1,6 +1,6 @@ -export const SUB_TASK_PREAMBLE = `[SUB-TASK MODE] You are a coordinated sub-task inside Parallel Code. A coordinator agent dispatched you to complete specific work. +export const SUB_TASK_PREAMBLE = `[SUB-TASK MODE] You are a coordinated sub-task inside Forge. A coordinator agent dispatched you to complete specific work. -You have two sub-task MCP tools available via the parallel-code server: +You have two sub-task MCP tools available via the forge server: - land_self — Happy-path finish line. Call this after committing your work and passing verification. The backend will merge your branch into the coordinator branch and clean up your task. - signal_done — Legacy/manual-review finish line. Use this only if the coordinator explicitly asks to review and land your branch manually. diff --git a/electron/mcp/types.ts b/electron/mcp/types.ts index 1b0044b4..308ebd4c 100644 --- a/electron/mcp/types.ts +++ b/electron/mcp/types.ts @@ -88,12 +88,12 @@ export interface CoordinatorState { propagateSkipPermissions: boolean; /** Path to the .mcp.json file written for this coordinator. */ mcpJsonPath: string; - /** True if Parallel Code created .mcp.json from scratch; false if it was pre-existing. */ + /** True if Forge created .mcp.json from scratch; false if it was pre-existing. */ createdMcpJson: boolean; - /** Previous value of mcpServers["parallel-code"] before this coordinator wrote its entry, if any. */ - previousMcpParallelCode?: unknown; - /** The value this coordinator wrote into mcpServers["parallel-code"]; used to detect concurrent edits on deregister. */ - writtenMcpParallelCode?: unknown; + /** Previous value of mcpServers["forge"] before this coordinator wrote its entry, if any. */ + previousMcpForge?: unknown; + /** The value this coordinator wrote into mcpServers["forge"]; used to detect concurrent edits on deregister. */ + writtenMcpForge?: unknown; } export interface SubtaskVerificationCheck { diff --git a/electron/vite.config.electron.ts b/electron/vite.config.electron.ts index c5d29107..1167413e 100644 --- a/electron/vite.config.electron.ts +++ b/electron/vite.config.electron.ts @@ -15,7 +15,7 @@ export default defineConfig({ watch: { // Creating git worktrees inside this repo would otherwise look like a giant // source-tree change to Vite in dev mode, causing the renderer to reload - // right when Parallel Code creates a task for itself. The function ignores + // right when Forge creates a task for itself. The function ignores // anything resolving outside the project root (e.g. host parent dirs). ignored: [ '**/.worktrees/**', diff --git a/index.html b/index.html index acc5439b..208a5e39 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Parallel Code + Forge